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
 5@pytest.mark.asyncio
 6async def test_async_expectations():
 7    with scenario.Scenario('awaitable test') as s:
 8        s.__await_on__.my_fake('some data') >> fake.Fake('another_fake')
 9        s.__await_on__.another_fake() >> fake.Fake('yet_another')
10        s.__await_on__.yet_another() >> Fake('last_one')
11        s.last_one.sync_func(1, 2, 3) >> 'sync value'
12
13        assert await my_code(Fake('my_fake')) == 'sync value'
14
15
16async def my_code(thing):
17    another = await thing('some data')
18    yet_another = await another()
19    last_one = await yet_another()
20    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
 7@pytest.fixture(autouse=True)
 8def override_import(patch_module):
 9    patch_module(async_read, 'aiofiles')
10
11
12@pytest.mark.asyncio
13async def test_read_write_from_async_file():
14    with scenario.Scenario() as s:
15        s.__async_with__.aiofiles.open('file_name.txt') >> Fake('the_file')
16        s.__await_on__.the_file.read() >> 'the text'
17
18        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
3
4async def go(filename):
5    async with aiofiles.open(filename) as f:
6        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()

AsyncIO Async For Loops

You can specify your expectation for an object to be used in an async for loop by using the __async_for__ modifier. This allows you to test code that uses async iterators. Here’s an example of a test with the code that passes it:

 1from testix import *
 2import pytest
 3
 4
 5@pytest.mark.asyncio
 6async def test_async_iterator():
 7    with scenario.Scenario() as s:
 8        s.__async_for__.my_iterator >> ['a', 'b', 'c']
 9
10        async_capitalizer = AsyncCapitalizer(Fake('my_iterator'))
11        result = []
12        async for item in async_capitalizer:
13            result.append(item)
14        assert result == ['A', 'B', 'C']
15
16
17class AsyncCapitalizer:
18    def __init__(self, iterable):
19        self._iterable = iterable
20
21    async def __aiter__(self):
22        async for item in self._iterable:
23            yield item.upper()

The __async_for__ modifier expects you to specify an iterable of values to be yielded when async for is used on your fake object.

NOTE

The __async_for__ modifier works directly on fake objects, not method calls. For example, s.__async_for__.alpha.beta() is not supported. If you need something like that, do it in stages:

s.alpha.beta() >> Fake('gamma')
s.__async_for__.gamma >> ['sequence', 'of', 'values']