Skip to content

Instantly share code, notes, and snippets.

@lflfm
Last active May 2, 2023 18:09
Show Gist options
  • Save lflfm/f6fc7b5d063d57f8860e82720f511e6a to your computer and use it in GitHub Desktop.
Save lflfm/f6fc7b5d063d57f8860e82720f511e6a to your computer and use it in GitHub Desktop.
Issue testing async method calls
# This gist is for a stack overflow question regarding the testing of the calls to async methods
#####################################
# Implementation to be tested
import asyncio
import json
import unittest
from unittest.mock import ANY, AsyncMock, MagicMock, Mock, patch
from uuid import uuid4
import websockets.client
class MyRemoteDeviceClass:
def __init__(self, ws_url, access_token):
self.access_token = access_token
self.ws_url = ws_url
async def enable_device(self, device_id):
call = {"method": "ExecuteCommand", "params": {"id": device_id, "commandId": "Enable"}, "id": str(uuid4())}
await self._run_command(call)
async def _run_command(self, call):
async with websockets.client.connect(self.ws_url, subprotocols=["aop.ipc"], extra_headers="auth and things") as ws:
await ws.send(json.dumps(call))
response = await ws.recv()
print("Response", response)
#####################################
# Helper functions
def async_test(coro):
"""Decorator to allow async tests.
https://stackoverflow.com/a/46324983/5651603
"""
def wrapper(*args, **kwargs):
loop = asyncio.new_event_loop()
try:
return loop.run_until_complete(coro(*args, **kwargs))
finally:
loop.close()
return wrapper
#####################################
# Unit tests
class SimpleAsyncTest(unittest.TestCase):
def setUp(self):
super().setUp()
self._prep_mock()
def _prep_mock(self):
# first we need a patcher for the class that we're going to mock
self.client_class_patcher = patch('websockets.client.connect')
# the mock_connect_class is the actual "class" that "replaces" the class
self.mock_connect_class = self.client_class_patcher.start()
# at the end of the tests, the original class must be restored (otherwise other tests might fail)
self.addCleanup(self.client_class_patcher.stop)
# then we need to "save" a MagickMock for the resulting instance of the class
self.connect_instance = MagicMock()
# mock the send and recv methods of out class instance
self.connect_instance.send.side_effect = MagicMock(return_value="fake_result_data")
self.connect_instance.recv.side_effect = MagicMock(return_value="fake_result_data")
# the return value of the mocked class is the MagickMock instance
self.mock_connect_class.return_value = self.connect_instance
@async_test
async def test_connection(self):
# arrange
_connector = MyRemoteDeviceClass("ws://nowhere:1234", "accesstoken")
# act
await _connector.enable_device("some_device_id")
# assert
self.mock_connect_class.assert_called_once_with("ws://nowhere:1234", subprotocols=["aop.ipc"], extra_headers=ANY)
# this test is working fine :)
@async_test
async def test_send_method_is_called(self):
# arrange
_connector = MyRemoteDeviceClass("ws://nowhere:1234", "accesstoken")
# act
await _connector.enable_device("some_device_id")
# assert
self.connect_instance.recv.assert_called_once() # this is not working :(
self.connect_instance.recv.assert_awaited_once() # this is also not working :(
# @TODO find out why neither of these assertions work...?
# the "print response" in the connector _run_command outputs this:
# Response <AsyncMock name='connect().__aenter__().recv()' id='139781044676176'>
# which indicates that it's using our mock, just not in the way we thought it would
if __name__ == '__main__':
unittest.main()
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment