Я создаю несколько тестов для кода python3, используя py.test. Код обращается к базе данных Postgresql с помощью aiopg (интерфейс на основе Asyncio для postgres).
Мои основные ожидания:
Каждый тестовый пример должен иметь доступ к новому циклу событий asyncio.
Тест, который выполняется слишком долго, остановится с исключением тайм-аута.
Каждый тестовый пример должен иметь доступ к соединению с базой данных.
Я не хочу повторяться при написании тестовых случаев.
Используя фикстуры py.test, я могу довольно близко подойти к тому, что хочу, но мне все равно приходится немного повторяться в каждом асинхронном тестовом примере.
Вот как выглядит мой код:
@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 для создания такого волшебного декоратора, однако кажется, что оба этих модуля помогают мне создать функцию с сигнатурой, идентичной my_test, что не является хорошим решением в этом случае.
Вероятно, это можно решить с помощью eval или аналогичного хака, но мне было интересно, есть ли что-то элегантное, чего мне здесь не хватает.