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()