AsyncIO Support

Testix offers support for testing asynchronous code, that is code which takes advantage of Python’s async and await keywords.

Testix’s async support has been tested to work with pytest-asyncio.

AsyncIO Expectations

You can specify that a method call should be awaited using the __await_on__ modifier of the Scenario, as in the example below. As you can see, you may also mix and match sync and async expectations.

 1from testix import *
 2import pytest
 3
 4@pytest.mark.asyncio
 5async def test_async_expectations():
 6    with scenario.Scenario('awaitable test') as s:
 7        s.__await_on__.my_fake('some data') >> fake.Fake('another_fake')
 8        s.__await_on__.another_fake() >> fake.Fake('yet_another')
 9        s.__await_on__.yet_another() >> Fake('last_one')
10        s.last_one.sync_func(1, 2, 3) >> 'sync value'
11
12        assert await my_code(Fake('my_fake')) == 'sync value'
13
14async def my_code(thing):
15        another = await thing('some data')
16        yet_another = await another()
17        last_one = await yet_another()
18        return last_one.sync_func(1, 2, 3)

Note that the test function itself is async and that you have to use the pytest.mark.asyncio decorator on the test - this decorator makes sure the test runs inside an asyncio event loop.

Note that the __await_on__ changes the expectation .my_fake('some data') into two expectations - the function call, and the use of await-ing. You can see this, if, e.g., you cut my_code(thing) short by replacing its first line with return 'sync value'. This is the correct value, so the assert statement passes, however the Scenario context will inform you that

E       Scenario ended, but not all expectations were met. Pending expectations (ordered):
[my_fake('some data'), await on my_fake('some data')@cb28287a42fc(),
another_fake(), await on another_fake()@94bb8afd7e45(),
yet_another(), await on yet_another()@b09a73bf3ee0(),
last_one.sync_func(1, 2, 3)]

You can see that every __await_on__ results in a special expectation representing it.

AsyncIO Context Managers

You can specify your expectation for an object to be used as an async context manager (i.e. in an async with statement) by using the __async_with__ modifier. Here’s an example testing a module called async_read which has an async function go() which reads the contents of a file asynchronously.

 1from testix import *
 2import pytest
 3
 4import async_read
 5
 6@pytest.fixture(autouse=True)
 7def override_import(patch_module):
 8    patch_module(async_read, 'aiofiles')
 9
10@pytest.mark.asyncio
11async def test_read_write_from_async_file():
12    with scenario.Scenario() as s:
13        s.__async_with__.aiofiles.open('file_name.txt') >> Fake('the_file')
14        s.__await_on__.the_file.read() >> 'the text'
15
16        assert 'the text' == await async_read.go('file_name.txt')

Note our use of patch_module to mock the aiofiles library, which we assume is imported and used by our async_read module.

The code which passes this test is

1import aiofiles
2
3async def go(filename):
4    async with aiofiles.open(filename) as f:
5        return await f.read()

Note you do not have to specify a return value with >> for the __async_with__ expectation if you want to use the “anonymous” form of the async with statement:

async with lock(): # no "as" part
    await handle_critical_data()