Sunday, February 25, 2018

[django-channels] Testing events from post_save signal

-----BEGIN PGP SIGNATURE-----

iQIzBAEBCgAdFiEEgp5wx8+ggeLmQKiikpDftiGHlegFAlqSyc8ACgkQkpDftiGH
legicA/+NZx78NWJ6KSTMW6PcqWiRXqZ63WsDj1E0QHJfi8MKoEXBO3nu4CV2gjT
BT91DkOJG2FChJZYSrdjIfvE9vl6S4d4kJKKCaaXRLEiQKWtVYMIJTN9knKleEId
J00nlltXJ8WhscbLGtEjFojz7jDGNy1MGzZR+wRo/dN/xswdcfJ8CBDod9ADrriL
iXv/VE5JX0CClKiq7ErtNnggjVQfPpimMQr6SVnmOPjKXL2OBo3AjLRosPdMTey3
otrYnqXUsFxGqP/zzyZfhY7jdy72HNLhSRs7Y25WxI78QTC2mhmcPfGoduR1QZ/s
Qm8spQzYVuLnz5GJzcW3QIKWxEStFKh8pSBmP/J18oFKjzMURbGCYdAcFUV9RWOT
sR9cQ8sikIJQGWczoIUUEPdmFc3IT19a2VaBunBV4TnD5GIaw5zo1zfA6OyOkQzR
HBiiZEeL/6jNIWlOmIp8S3JgYjH6FriTveu3AM65AXVHLByndTd9PZ71GWfs351K
D+srrCmYVOYBtCPZoifTh5kP7sY0LgEzmeOfwwTUCg0/RS9szNeZ5lUg02bWroOh
MBu2coKfZAkNqpE6Z7gOSruu5hj3thOrqmRiGfg6MdcTJJG0g081WMIs6bVwKOgR
JhJN4yG58t6uO2n3I557vgZKMyeVwG+0uUKy+rqruABHA20LCxs=
=q2ca
-----END PGP SIGNATURE-----
Hello,
I've just migrated my project to django-channels 2.x. Thanks to everyone involved!

I'm trying to write a test for a consumer.
I have a post_save signal receiver, which sends a message to a group. As I understand,
I need to wrap `group_send` with `async_to_sync` because django signals can't be
async functions:

def notify_on_model_changes(model):
    from django.contrib.contenttypes.models import ContentType
    ct = ContentType.objects.get_for_model(model)
    model_label = '.'.join([ct.app_label, ct.model])

    channel_layer = get_channel_layer()
    group_send = async_to_sync(channel_layer.group_send)

    def receiver(sender, instance, **kwargs):
        payload = {
            'type': 'model.changed',
            'pk': instance.pk,
            'model': model_label
        }     
        group_send(f'django.{model_label}', payload)

    post_save.connect(receiver, sender=model, weak=False,
                      dispatch_uid=f'django.{model_label}')

# in AppConfig.ready:
# notify_on_model_changes(Conversation)



My test suite, however, is async function:

@pytest.fixture
async def communicator():
    communicator = WebsocketCommunicator(GraphqlSubcriptionConsumer, "/")
    await communicator.connect()
    yield communicator
    await communicator.disconnect()
    
async def test_subscription_start(communicator):
    def make_conversation():
        return Conversation.objects.create()

    # function body truncated
    # subscribe for changes in Conversation model
    await communicator.send_json_to(data)

    conversation = await sync_to_async(make_conversation)()
    response = await communicator.receive_json_from()
    assert response['type'] == 'data'

I can't use `Conversation.objects.create()` directly, because it uses `async_to_sync`.
First, I need to convert it to async and await the result. I kinda feel this is hackish
jumping from async to sync and back to async, but so far everything works as expected
and test works.


Here comes the punchline:
The tests fail to teardown cleanly, because apparently there's hanging DB connection
and after a while pytest just fails with `There is 1 other session using the database.`.

Breadcrumbs:
1. If I comment out last three lines of test (make_conversations and waiting for result),
the test exits cleanly - seems like there's no problem with passing `sync_to_async` function
to `post_save.connect`.

2. If I create `async_make = sync_to_async(make_conversation)`, but don't call it at all,
the test exists cleanly - I thought that there might be problem with calling `async_to_sync`
inside code wrapped with `sync_to_async`.


I suspect there's a hanging db connection which isn't cleaned and/or garbage collected.
I would also appreciate any comments about structure of such tests - is there cleaner way
test django signals inside async test cases?


Cheers,
   Tom

No comments:

Post a Comment