Skip to content

Commit

Permalink
Pytest async fixtures (#2226)
Browse files Browse the repository at this point in the history
* Accept async fixtures in pytest plugin (#2223)

* Add tests for async fixtures (#2223)

* Update changelog

* Update CONTRIBUTORS.txt

* Update helpers.py
  • Loading branch information
k4nar authored and asvetlov committed Sep 17, 2017
1 parent b1f5afd commit 3f94507
Show file tree
Hide file tree
Showing 5 changed files with 181 additions and 0 deletions.
1 change: 1 addition & 0 deletions CONTRIBUTORS.txt
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,7 @@ Will McGugan
Willem de Groot
Wilson Ong
Yannick Koechlin
Yannick Péroux
Yegor Roganov
Young-Ho Cha
Yuriy Shatrov
Expand Down
7 changes: 7 additions & 0 deletions aiohttp/helpers.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import cgi
import datetime
import functools
import inspect
import os
import re
import sys
Expand Down Expand Up @@ -237,6 +238,12 @@ def current_task(loop=None):
return task


def isasyncgenfunction(obj):
if hasattr(inspect, 'isasyncgenfunction'):
return inspect.isasyncgenfunction(obj)
return False


def parse_mimetype(mimetype):
"""Parses a MIME type into its components.
Expand Down
57 changes: 57 additions & 0 deletions aiohttp/pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
import pytest
from py import path

from aiohttp.helpers import isasyncgenfunction
from aiohttp.web import Application

from .test_utils import unused_port as _unused_port
Expand Down Expand Up @@ -37,6 +38,62 @@ def pytest_addoption(parser):
help='enable event loop debug mode')


def pytest_fixture_setup(fixturedef, request):
"""
Allow fixtures to be coroutines. Run coroutine fixtures in an event loop.
"""
func = fixturedef.func

if isasyncgenfunction(func):
# async generator fixture
is_async_gen = True
elif asyncio.iscoroutinefunction(func):
# regular async fixture
is_async_gen = False
else:
# not an async fixture, nothing to do
return

strip_request = False
if 'request' not in fixturedef.argnames:
fixturedef.argnames += ('request',)
strip_request = True

def wrapper(*args, **kwargs):
request = kwargs['request']
if strip_request:
del kwargs['request']

# if neither the fixture nor the test use the 'loop' fixture,
# 'getfixturevalue' will fail because the test is not parameterized
# (this can be removed someday if 'loop' is no longer parameterized)
if 'loop' not in request.fixturenames:
raise Exception(
"Asynchronous fixtures must depend on the 'loop' fixture or "
"be used in tests depending from it."
)

_loop = request.getfixturevalue('loop')

if is_async_gen:
# for async generators, we need to advance the generator once,
# then advance it again in a finalizer
gen = func(*args, **kwargs)

def finalizer():
try:
return _loop.run_until_complete(gen.__anext__())
except StopAsyncIteration: # NOQA
pass

request.addfinalizer(finalizer)
return _loop.run_until_complete(gen.__anext__())
else:
return _loop.run_until_complete(func(*args, **kwargs))

fixturedef.func = wrapper


@pytest.fixture
def fast(request):
""" --fast config option """
Expand Down
1 change: 1 addition & 0 deletions changes/2223.feature
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
Accept coroutine fixtures in pytest plugin
115 changes: 115 additions & 0 deletions tests/test_pytest_plugin.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

import pytest

from aiohttp.pytest_plugin import LOOP_FACTORIES


pytest_plugins = 'pytester'

Expand Down Expand Up @@ -180,3 +182,116 @@ async def test_bad():
stdout, _ = capsys.readouterr()
assert ("test_warning_checks.py:__LINE__:coroutine 'foobar' was "
"never awaited" in re.sub('\d{2,}', '__LINE__', stdout))


def test_aiohttp_plugin_async_fixture(testdir, capsys):
testdir.makepyfile("""\
import asyncio
import pytest
from aiohttp import web
pytest_plugins = 'aiohttp.pytest_plugin'
@asyncio.coroutine
def hello(request):
return web.Response(body=b'Hello, world')
def create_app(loop):
app = web.Application()
app.router.add_route('GET', '/', hello)
return app
@pytest.fixture
@asyncio.coroutine
def cli(test_client):
client = yield from test_client(create_app)
return client
@pytest.fixture
@asyncio.coroutine
def foo():
return 42
@pytest.fixture
@asyncio.coroutine
def bar(request):
# request should be accessible in async fixtures if needed
return request.function
@asyncio.coroutine
def test_hello(cli):
resp = yield from cli.get('/')
assert resp.status == 200
def test_foo(loop, foo):
assert foo == 42
def test_foo_without_loop(foo):
# will raise an error because there is no loop
pass
def test_bar(loop, bar):
assert bar is test_bar
""")
nb_loops = len(LOOP_FACTORIES)
result = testdir.runpytest('-p', 'no:sugar')
result.assert_outcomes(passed=3 * nb_loops, error=1)
result.stdout.fnmatch_lines(
"*Asynchronous fixtures must depend on the 'loop' fixture "
"or be used in tests depending from it."
)


@pytest.mark.skipif(sys.version_info < (3, 6), reason='old python')
def test_aiohttp_plugin_async_gen_fixture(testdir):
testdir.makepyfile("""\
import asyncio
import pytest
from unittest import mock
from aiohttp import web
pytest_plugins = 'aiohttp.pytest_plugin'
canary = mock.Mock()
async def hello(request):
return web.Response(body=b'Hello, world')
def create_app(loop):
app = web.Application()
app.router.add_route('GET', '/', hello)
return app
@pytest.fixture
async def cli(test_client):
yield await test_client(create_app)
canary()
async def test_hello(cli):
resp = await cli.get('/')
assert resp.status == 200
def test_finalized():
assert canary.called is True
""")
nb_loops = len(LOOP_FACTORIES)
result = testdir.runpytest('-p', 'no:sugar')
result.assert_outcomes(passed=1 * nb_loops + 1)

0 comments on commit 3f94507

Please sign in to comment.