py.test混合装置和asyncio协同程序

时间:2015-02-02 17:37:37

标签: postgresql fixtures pytest python-decorators python-asyncio

我正在使用py.test为python3代码构建一些测试。代码使用aiopg(基于Asyncio的postgres接口)访问Postgresql数据库。

我的主要期望:

  • 每个测试用例都应该有权访问新的asyncio事件循环。

  • 运行时间过长的测试将因超时异常而停止。

  • 每个测试用例都应该有权访问数据库连接。

  • 编写测试用例时,我不想重复自己。

使用py.test fixtures我可以非常接近我想要的东西,但我仍然需要在每个异步测试用例中重复一遍。

这就是我的代码的样子:

@pytest.fixture(scope='function')
def tloop(request):
    # This fixture is responsible for getting a new event loop
    # for every test, and close it when the test ends.
    ...

def run_timeout(cor,loop,timeout=ASYNC_TEST_TIMEOUT):
    """
    Run a given coroutine with timeout.
    """
    task_with_timeout = asyncio.wait_for(cor,timeout)
    try:
        loop.run_until_complete(task_with_timeout)
    except futures.TimeoutError:
        # Timeout:
        raise ExceptAsyncTestTimeout()


@pytest.fixture(scope='module')
def clean_test_db(request):
    # Empty the test database.
    ...

@pytest.fixture(scope='function')
def udb(request,clean_test_db,tloop):
    # Obtain a connection to the database using aiopg
    # (That's why we need tloop here).
    ...


# An example for a test:
def test_insert_user(tloop,udb):
    @asyncio.coroutine
    def insert_user():
        # Do user insertion here ...
        yield from udb.insert_new_user(...
        ...

    run_timeout(insert_user(),tloop)

我可以使用到目前为止的解决方案,但是定义内部协程并为我编写的每个异步测试添加run_timeout行会很麻烦。

我希望我的测试看起来像这样:

@some_magic_decorator
def test_insert_user(udb):
    # Do user insertion here ...
    yield from udb.insert_new_user(...
    ...

我尝试以一种优雅的方式创建这样的装饰器,但失败了。更一般地说,如果我的测试看起来像:

@some_magic_decorator
def my_test(arg1,arg2,...,arg_n):
    ...

然后生成的函数(在应用装饰器之后)应该是:

def my_test_wrapper(tloop,arg1,arg2,...,arg_n):
    run_timeout(my_test(),tloop)

请注意,我的一些测试使用其他灯具(例如除了udb),并且这些灯具必须显示为生成函数的参数,否则py.test将不会调用它们。

我尝试使用wraptdecorator python模块来创建这样一个魔术装饰器,但似乎这两个模块都可以帮助我创建一个与my_test相同的签名函数,这不是在这种情况下,这是一个很好的解决方案。

这可能解决了使用eval或类似的黑客攻击,但我想知道是否有一些我在这里缺少的优雅。

2 个答案:

答案 0 :(得分:5)

我目前正在尝试解决类似的问题。这是我到目前为止所提出的。它似乎工作但需要一些清理:

# tests/test_foo.py
import asyncio

@asyncio.coroutine
def test_coro(loop):
    yield from asyncio.sleep(0.1)
    assert 0

# tests/conftest.py
import asyncio


@pytest.yield_fixture
def loop():
    loop = asyncio.new_event_loop()
    asyncio.set_event_loop(loop)
    yield loop
    loop.close()


def pytest_pycollect_makeitem(collector, name, obj):
    """Collect asyncio coroutines as normal functions, not as generators."""
    if asyncio.iscoroutinefunction(obj):
        return list(collector._genfunctions(name, obj))


def pytest_pyfunc_call(pyfuncitem):
    """If ``pyfuncitem.obj`` is an asyncio coroutinefunction, execute it via
    the event loop instead of calling it directly."""
    testfunction = pyfuncitem.obj

    if not asyncio.iscoroutinefunction(testfunction):
        return

    # Copied from _pytest/python.py:pytest_pyfunc_call()
    funcargs = pyfuncitem.funcargs
    testargs = {}
    for arg in pyfuncitem._fixtureinfo.argnames:
        testargs[arg] = funcargs[arg]
    coro = testfunction(**testargs)  # Will no execute the test yet!

    # Run the coro in the event loop
    loop = testargs.get('loop', asyncio.get_event_loop())
    loop.run_until_complete(coro)

    return True  # TODO: What to return here?

所以我基本上让pytest收集像正常函数一样的asyncio协同程序。我还截取文本exectuion的功能。如果要测试的函数是协程,我在事件循环中执行它。它可以使用或不使用fixture,每次测试都会创建一个新的事件循环实例。

编辑:根据Ronny Pfannschmidt的说法,在2.7版本发布之后,这样的东西将被添加到pytest中。 : - )

答案 1 :(得分:-1)

  

每个测试用例都应该有权访问新的asyncio事件循环。

asyncio的测试套件使用unittest.TestCase。它使用setUp()方法创建一个新的事件循环。 addCleanup(loop.close)自动关闭事件循环,即使出错也是如此。

抱歉,如果你不想使用TestCase,我不知道如何用py.test写这个。但如果我没记错的话,py.test支持unittest.TestCase。

  

运行时间过长的测试将因超时异常而停止。

您可以将loop.call_later()与一个将BaseException引发为监视狗的函数一起使用。