Advanced Features

This chapter describes miscellaneous features of asynctest which can be leveraged in specific use cases.

Controlling time

Tests running calls to asyncio.sleep() will take as long as the sum of all these calls. These calls are frequent when testing for timeouts, for instance.

In many cases, this will add a useless delay to the execution of the test suite, and encourage us to deactivate or ignore these tests.

ClockedTestCase is a subclass of TestCase which allows to advance the clock of the loop in a test with the coroutine advance().

This will not affect the wall clock: functions like time.time() or datetime.datetime.now() will return the regular date and time of the system.

class TestAdvanceTime(asynctest.ClockedTestCase):
    async def test_advance_time(self):
        base_loop_time = self.loop.time()
        base_wall_time = time.time()

        await self.advance(10)

        self.assertEqual(base_loop_time + 10, self.loop.time())
        self.assertTrue(is_time_around(base_wall_time))

This example is pretty self-explanatory: we verified that the clock of the loop advanced as expected, without awaiting 10 actual seconds and changing the time of the wall clock.

Internally, ClockedTestCase will ensure that the loop is executed as if time was passing fast, instead of jumping the clock to the target time.

class TestWithClockAndCallbacks(asynctest.ClockedTestCase):
    results = None

    def runs_at(self, expected_time):
        self.results.append(is_time_around(expected_time, self.loop))

    @asynctest.fail_on(active_handles=True)
    async def test_callbacks_executed_when_expected(self):
        self.results = []

        base_time = self.loop.time()
        self.loop.call_later(1, self.runs_at, base_time + 1)
        self.loop.call_at(base_time + 7, self.runs_at, base_time + 7)

        # This shows that the callback didn't run yet
        self.assertEqual(0, len(self.results))

        await self.advance(10)

        # This shows that the callbacks ran...
        self.assertEqual(2, len(self.results))
        # ...when expected
        self.assertTrue(all(self.results))

This example schedules function calls to be executed later by the loop. Each call will verify that it runs at the expected time. @fail_on(active_handles=True) ensures that the callbacks have been executed when the test finishes.

The source code of is_time_around() can be found in the example file tutorial/clock.py.

Mocking I/O

Testing libraries or functions dealing with low-level IO objects may be complex: these objects are outside of our control, since they are owned by the kernel. It can be impossible to exactly predict their behavior and simulate edge-cases, such as the ones happening in a real-world scenario in a large network.

Even worse, using mocks in place of files will often raise OSError because these obhjects are not compatible with the features of the system used by the loop.

asynctest provides special mocks which can be used in place of actual file-like objects. They are supported by the loop provided by TestCase if the loop uses a standard implementation with a selector (Window’s Proactor loop or uvloop are not supported).

These mocks are configured with a spec matching common file-like objects.

Mock spec
FileMock a file object, implements fileno()
SocketMock socket.socket
SSLSocketMock ssl.SSLSocket

We can use asynctest.isfilemock() to differenciate mocks from regular objects.

As of asynctest 0.12, these mocks don’t provide other features, and must be configured to return expected values for calls to methods like read() or recv().

When configured, we still need to force the loop to detect that I/O is possible on these mock files.

This is done with set_read_ready() and set_write_ready().

class TestMockASocket(asynctest.TestCase):
    async def test_read_and_write_from_socket(self):
        socket_mock = asynctest.SocketMock()
        socket_mock.type = socket.SOCK_STREAM

        recv_data = iter((
            b"some data read",
            b"some other",
            b" ...and the last",
        ))

        recv_buffer = bytearray()

        def recv_side_effect(max_bytes):
            nonlocal recv_buffer

            if not recv_buffer:
                try:
                    recv_buffer.extend(next(recv_data))
                    asynctest.set_read_ready(socket_mock, self.loop)
                except StopIteration:
                    # nothing left
                    pass

            data = recv_buffer[:max_bytes]
            recv_buffer = recv_buffer[max_bytes:]

            if recv_buffer:
                # Some more data to read
                asynctest.set_read_ready(socket_mock, self.loop)

            return data

        def send_side_effect(data):
            asynctest.set_read_ready(socket_mock, self.loop)
            return len(data)

        socket_mock.recv.side_effect = recv_side_effect
        socket_mock.send.side_effect = send_side_effect

        reader, writer = await asyncio.open_connection(sock=socket_mock)

        writer.write(b"a request?")
        self.assertEqual(b"some", await reader.read(4))
        self.assertEqual(b" data read", await reader.read(10))
        self.assertEqual(b"some other ...and the last", await reader.read())

In this example, we configure a socket mock to simulate a simple request-response scenario with a TCP (stream) socket. Some data is available to read on the socket once a request has been written. recv_side_effect() makes as if the data is received in several packets, but it has no impact on the high level StreamReader.

It’s common that while a read operation blocks until data is available, a write is often successful. Thus, we didn’t bother simulating the case where the congestion control would block the write operation.

Testing with event loop policies

Advanced users may not be able to use the loop provided by TestCase because they use a customized event loop policy (see Policies). It is often the case when using an alternative implementation (like uvloop) or if the tests are integrated within a framework hidding the scheduling and management of the loop.

It is possible to force the TestCase to use the loop provided by the policy by setting the class attribute use_default_loop.

Conversely, authors of libraries may not want to assume which loop they should use and let users explicitly pass the loop as argument to a function call. For instance, most of the high-level functions of asyncio (see Streams, for instance) allow the caller to specify the loop to use if it needs this kind of flexibility.

forbid_get_event_loop forbids the use of asyncio.get_event_loop(). An exception is raised if the method is called while a test is running. It helps developers to ensure they don’t rely on the default loop this their library.

Note

The behavior of asyncio.get_event_loop() changed over time. Explicitly passing the loop is not the recommended practice anymore.