Using Django and Channels 2, I have a consumer method that can can be accessed through channel groups and that may raise exceptions. Like this trivial one:
from channels.generic.websocket import WebsocketConsumer
from asgiref.sync import async_to_sync
class DummyConsumer(WebsocketConsumer):
def connect(self):
async_to_sync(self.channel_layer.group_add)(
"dummy",
self.channel_name,
)
self.accept()
def will_raise(self, event):
raise ValueError('value error')
def disconnect(self, code):
async_to_sync(self.channel_layer.group_discard)(
"dummy",
self.channel_name,
)
I want to test this method using pytest-asyncio. Since one can catch the exception of a coroutine with pytest.raises
, I thought naively that something like this would be enough:
import pytest
from channels.testing import WebsocketCommunicator
from channels.layers import get_channel_layer
from app.consumers import DummyConsumer
channel_layer = get_channel_layer()
@pytest.fixture
async def communicator():
communicator = WebsocketCommunicator(DummyConsumer, "ws/dummy/")
await communicator.connect()
yield communicator
await communicator.disconnect()
@pytest.mark.asyncio
async def test_will_raise(communicator):
with pytest.raises(ValueError):
await channel_layer.group_send('dummy', {
'type': 'will_raise'
})
But the test fails in a pretty confusing way (truncated output):
================== ERRORS ==================
___ ERROR at teardown of test_will_raise ___
...
> raise ValueError('value error')
E ValueError: value error
app/consumers.py:28: ValueError
================= FAILURES =================
_____________ test_will_raise ______________
...
await channel_layer.group_send('dummy', {
> 'type': 'will_raise'
})
E Failed: DID NOT RAISE <class 'ValueError'>
app/tests_dummy.py:21: Failed
==== 1 failed, 1 error in 1.47 seconds =====
So, what should I do? Is the raising of an exception from a consumer method a bad design?
A
channel_layer
has two sites. One site, that sends data into thechannel_layer
and the other site, that receives the data. The sending site does not get any response from the receiving site. This means, if the receiving site raises an exception, the sending site does not see it.In your test, you are testing the sending site. It sends a message to the
channel_layer
, but as explained this does not raises the exception.To test that the exception is raised, you have to write a test that connects to your consumer. It could look like this:
As you can see, the exception does not happen when you send into the
channel_layer
, but on the communicator, that listens on thechannel_layer
. See also: https://channels.readthedocs.io/en/latest/topics/testing.html#waitAlso note, that the test does not call
communicator.disconnect()
. When an exception happens inside the communicator,disconnect()
doesn't have to be called. See the second sentence in the green "Important" box beneath this headline: https://channels.readthedocs.io/en/latest/topics/testing.html#websocketcommunicator