Twisted tests will generally look like:
@twisted_utils.reacts # marks the test as needing a reactor
# marks the function as a coroutine, a poor immitation of a Python3 async/def
# coroutine
@twisted_utils.inlineCallbacks
def test_foo(fixtures)
server = Server()
assert (yield server.message("ping")) == "pong"
assert (yield server.message("GET resource")) == "4"
If you see:
def wrapper(**kwargs):
__tracebackhide__ = True
> assert func(**kwargs) is None
E assert <Deferred at 0xdeadbeef> is None
E + where <Deferred at 0xdeadbeef> = <function test_foo at 0x>bbadbeef(**{})
this means the test completed without testing anything! Similar to not awaiting
a task/coroutine, you need to add the @twisted_utils.reacts
to run your test
and extract the result
If you see a TwistedTestUtilError. See the associated advice in test.utils.twisted
-
why are there two decorators? the decorators do different things, twisted_utils.inlineCallbacks turns a coroutine into a function returning a Deferred, twisted_utils.reacts takes a function that returns a Deferred and extracts the result by running a twisted reactor. It's similar to the following from py3.4:
@pytest.mark.asyncio @asyncio.coroutine def test_bad_because_legacy(): """ Support for generator-based coroutines is deprecated and is scheduled for removal in Python 3.10. """ yield from asyncio.sleep(1)
-
why not pytest-twisted?
- pytest-twisted runs one reactor for the entire test suite, this means sockets that are awaited, LoopingCalls and other timers persist for the whole test suite.
- pytest-twisted pauses and resumes the reactor using a greenlet which defeats the entire purpose of using a reactor/event loop for explicit non-blocking IO.
- pytest-twisted runs the reactor with installSignalHandlers=True this means that KeyboardInterrupt and the jenkins interrupt signal will be ignored and will not stop the suite.
when managing async contexts you need to await on all of the "enter", "body" and "exit" phases:
eg in python3 you can just create an async contextmanager:
@contextlib.asynccontextmanager
def some_service():
service = SomeService()
await service.startService()
try:
yield service
# service is now active only inside the context managed with `async with`
finally:
await service.stopService()
@pytest.mark.asyncio
async def test_foo():
v = "spam"
# nested contexts can be managed with a single `async with` statement
async with some_service() as service, other_service(service) as other_service:
await other_service.recvText(v)
assert await service.recvText("foo") == v
however in Python2 we don't have async context managers so we can either use a blocking context manager with "pytest_twisted.blockon" or we need to build a context using continuation passing style (CPS):
@defer.inlineCallbacks
def with_some_service(fn):
service = SomeService()
yield service.startService()
try:
defer.returnValue((yield fn(service)))
# service is now active only inside the context managed by the continuation
finally:
yield service.stopService()
@twisted_utils.reacts
def test_foo():
v = "spam"
@defer.inlineCallbacks
def test(some_service, other_service):
yield other_service.recvText(v)
assert (yield service.recvText("foo")) == v
# to manage nested contexts ...
return with_some_service(
# ... the context must be passed from one continuation ...
lambda some_service: with_other_service(
# ... to the next ...
service=service,
# ... all the way to the test
fn=lambda other_service: test(some_service, other_service)
)
)
- Examples of this sort of CPS context management for twisted can be found in the wild here: https://txpostgres.readthedocs.io/en/latest/txpostgres.html#txpostgres.txpostgres.Connection.runInteraction
- some of you may also remember that this is how contexts were managed back in Py 2.4
- CPS is used over
pytest_twisted.blockon
because pytest_twisted runs one twisted reactor in a greenlet for the entire test suite. - Once the code is only running on Py 3 we can replace all this complication with nice async-with context managers
heavily inspired by https://github.com/sqlalchemy/sqlalchemy/blob/d933ddd503a1ca0a7c562c51c503139c541e707e/lib/sqlalchemy/orm/scoping.py#L160-L204