py.test смесителни тела и асинхронни съпрограмми

Създавам някои тестове за код на python3, използвайки py.test. Кодът осъществява достъп до база данни на Postgresql с помощта на aiopg (базиран на Asyncio интерфейс към postgres).

Основните ми очаквания:

  • Всеки тестов случай трябва да има достъп до нов асинхронен цикъл на събития.

  • Тест, който работи твърде дълго, ще спре с изключение за изчакване.

  • Всеки тестов случай трябва да има достъп до връзка с база данни.

  • Не искам да се повтарям, когато пиша тестовите случаи.

Използвайки 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 няма да ги извика.

Опитах да използвам както wrapt, така и decorator python модули за създаване на такъв магически декоратор, но изглежда, че и двата модула ми помагат да създам функция със сигнатура, идентична на my_test, което не е добро решение в такъв случай.

Това вероятно може да бъде разрешено с помощта на eval или подобен хак, но се чудех дали има нещо елегантно, което пропускам тук.


person real    schedule 02.02.2015    source източник


Отговори (2)


В момента се опитвам да реша подобен проблем. Ето какво измислих досега. Изглежда, че работи, но има нужда от известно почистване:

# 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 съпрограми като нормални функции. Също така прихващам изпълнението на текст за функции. Ако функцията, която трябва да бъде тествана, е съпрограма, аз я изпълнявам в цикъла на събитията. Работи със или без приспособление, създаващо нов екземпляр на цикъл на събитие за всеки тест.

Редактиране: Според Ronny Pfannschmidt нещо подобно ще бъде добавено към pytest след версията 2.7. :-)

person Stefan Scherfke    schedule 17.02.2015
comment
Не знаех за pytest_pycollect_makeitem и pytest_pyfunc_call. Много готино! Откъде ги взе тези неща? Ако планирате да използвате това, уверете се, че имате различен цикъл за всеки тест и добавете моята функция run_timeout към вашия код, за да направите тестовете ви да не блокират. - person real; 18.02.2015
comment
От документите на pytest (pytest.org/latest/plugins.html#pytest-hook -референция), от ровене в кода и от отгатване. IMHO наистина не е толкова добре документиран. Може би ще го пусна като официален pytest-plugin. - person Stefan Scherfke; 18.02.2015

Всеки тестов случай трябва да има достъп до нов асинхронен цикъл на събития.

Тестовият пакет на asyncio използва unittest.TestCase. Той използва метода setUp() за създаване на нов цикъл на събития. addCleanup(loop.close) затваря автоматично цикъла на събитията, дори при грешка.

Съжалявам, не знам как да напиша това с py.test, ако не искате да използвате TestCase. Но ако си спомням правилно, py.test поддържа unittest.TestCase.

Тест, който работи твърде дълго, ще спре с изключение за изчакване.

Можете да използвате loop.call_later() с функция, която повдига BaseException като куче-пазач.

person haypo    schedule 04.02.2015
comment
Благодаря за отговора. Наличието на нов цикъл на събитие и изключения за изчакване са неща, които вече съм разрешил. Вижте функциите tloop и run_timeout в моя код. Проблемът ми е със създаването на магическия декоратор. Искам всеки тестов случай да бъде сърутина, която просто ще се изпълнява автоматично. - person real; 05.02.2015