Combines py.test and trio/curio
I’ll combine pytest and trio (or curio, if that’s easier) to write my test cases as coroutine functions. This is relatively easy to achieve by declaring a custom test runner in conftest.py
:
@pytest.mark.tryfirst
def pytest_pyfunc_call(pyfuncitem):
'''If item is a coroutine function, run it under trio'''
if not inspect.iscoroutinefunction(pyfuncitem.obj):
return
kernel = trio. Kernel()
funcargs = pyfuncitem.funcargs
testargs = {arg: funcargs[arg]
for arg in pyfuncitem._fixtureinfo.argnames}
try:
kernel.run(functools.partial(pyfuncitem.obj, **testargs))
finally:
kernel.run(shutdown=True)
return True
This allows me to write test cases like this:
async def test_something():
server = MockServer()
server_task = await trio.run(server.serve)
try:
# test the server
finally:
server.please_terminate()
try:
with trio.fail_after(30):
server_task.join()
except TooSlowError:
server_task.cancel()
But this is a lot of boilerplate. In non-asynchronous code, I’d break it down into fixtures:
@pytest.yield_fixture()
def mock_server():
server = MockServer()
thread = threading. Thread(server.serve)
thread.start()
try:
yield server
finally:
server.please_terminate()
thread.join()
server.server_close()
def test_something(mock_server):
# do the test..
Is there a way to do the same thing in trio, i.e. implement an async device? Ideally, I would write:
async def test_something(mock_server):
# do the test..
Solution
EDIT: The answers below are mostly irrelevant right now — instead use pytest-trio and follow the instructions instructions in its manual
Your example pytest_pyfunc_call
code doesn’t work because it’s a mix of trio and curio :-). For trio, there’s a decorator trio.testing.trio_test
that you can use to mark individual tests (like if you’re using classic unittest or something else), so the easiest way to write a pytest plugin function is to simply apply it to each asynchronous test:
from trio.testing import trio_test
@pytest.mark.tryfirst
def pytest_pyfunc_call(pyfuncitem):
if inspect.iscoroutine(pyfuncitem.obj):
# Apply the @trio_test decorator
pyfuncitem.obj = trio_test(pyfuncitem.obj)
If you’re curious, this basically equates:
import trio
from functools import wraps, partial
@pytest.mark.tryfirst
def pytest_pyfunc_call(pyfuncitem):
if inspect.iscoroutine(pyfuncitem.obj):
fn = pyfuncitem.obj
@wraps(fn)
def wrapper(**kwargs):
trio.run(partial(fn, **kwargs))
pyfuncitem.obj = wrapper
In any case, this will not solve your fixture problem – because you need more involvement.