Test that a consumer method can raise an exception with Django Channels and pytest-asyncio

2.5k views Asked by At

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?

1

There are 1 answers

3
ostcar On BEST ANSWER

A channel_layer has two sites. One site, that sends data into the channel_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:

channel_layer = get_channel_layer()

@pytest.mark.asyncio
async def test_will_raise():
    communicator = WebsocketCommunicator(DummyConsumer, "ws/dummy/")
    await communicator.connect()

    await channel_layer.group_send('dummy', {
            'type': 'will_raise'
        })

    with pytest.raises(ValueError):
        await communicator.wait()

As you can see, the exception does not happen when you send into the channel_layer, but on the communicator, that listens on the channel_layer. See also: https://channels.readthedocs.io/en/latest/topics/testing.html#wait

Also 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

You do not, however, have to disconnect() if your app already raised an error.