3

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?

Neraste
  • 485
  • 4
  • 15

1 Answers1

2

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.

ostcar
  • 161
  • 4
  • Ok, this enlightened me. However, since `communicator.disconnect` is never called, neither is `self.channel_layer.group_discard`, which means the groups still exists. So, the test is not cleanly exited. Or, should I do it myself? – Neraste Nov 11 '18 at 14:55
  • There is no need to discard a channel from a group in a test. Since nobody is listening to the old channel, it does no harm, if the group sends messages to it. – ostcar Nov 12 '18 at 18:46
  • The fact is, the channel group is not emptied between tests (as far as I know), which means in this case tests are not independent anymore. In my real code, for some reasons, I check in `connect` if the channel does not already exist in the group; consequently all tests after `test_will_raise` fail. So the solution for me is to discard the channel manually from channel group. I think this is due to my specific case, but maybe a channel group cleaning could be useful. – Neraste Nov 14 '18 at 07:40