Последнее время я все чаще работаю с популярной библиотекой для тестирования - py.test. В компании, где я работаю, мы полностью перешли на ее использование в написании, как unit тестов, так и интеграционных.
Чем “крут” py.test?
Из главных фишек py.test можно выделить следующие:
- Раскрытие asssert’ов - при выводе ошибки, он красиво показывает, что и с чем мы сравнили, выводит типы и доходчиво это выводит, пряча весь traceback, который привел к assert’у, а также пытаясь самостоятельно вывести уточняющий текст.
- Система фикстур - специальные функции, которые неявно передаются в тестируемую функцию. Является очень мощным инструментом, т.к. позволяет делать глобальные/локальные setup/teardown вещи, легко переносимые между тестами.
- Система плагинов построеная на хуках, с помощью которой очень просто писать дополнительные плагины. В данный момент, есть плагины реализующие практически все, что угодно (от дополнительных assert’ов и выводилок на экран, до мощных инструментов, таких как bdd, coverage и т.п.)
- Возможность распределенного запуска тестов в автоматическом режиме и на разных машинах
Раскрытие assert’ов
Возьмем для примера самый простой случай:
def test_simple():
one = 1
one_str = '1'
assert one_str == one
И выполним сначала nosetests:
nosetests test.py
F
======================================================================
FAIL: test.test_simple
----------------------------------------------------------------------
Traceback (most recent call last):
File "/Users/gigimon/workspace/python/scalr3/lib/python3.4/site-packages/nose/case.py", line 198, in runTest
self.test(*self.arg)
File "/Users/gigimon/workspace/python/scalr3/test.py", line 4, in test_simple
assert one_str == one
AssertionError
----------------------------------------------------------------------
Ran 1 test in 0.002s
FAILED (failures=1)
И видим стандартный traceback, где абсолютно не понятно, что с чем сравнилось и, что пошло не так. Теперь же py.test:
py.test test.py
====================
test session starts
====================
collected 1 items
test.py F
===== FAILURES =====
___ test_simple ___
def test_simple():
one = 1
one_str = '1'
> assert one_str == one
E assert '1' == 1
test.py:4: AssertionError
===== 1 failed
И тут мы сразу видим наш исходный код теста, конкретную строку, где упал тест, а также сравниваемые величины в понятном виде.
Система фикстур
Фикстуры - это функции, которые обернуты в декоратор @pytest.fixture и делающие setUp перед запуском теста, где их используют и teardown после выполнения теста. Также, их можно использовать и в самом тесте, если они что-либо возвращают. Помимо этого они могут выполняться в разном контексте исполнения: для каждой функции, для каждого модуля, для всей сессии тестирования, это значит, что если использовать в каждом тесте фикстуру, которая исполняется только на уровне сессии, то в каждую тест-функцию будет приходить 1 и тот же объект.
Рассмотрим пример из официальной документации, где фикстура подключается к smtp серверу и завершает коннект после каждого теста:
import smtplib
import pytest
@pytest.fixture(scope="module")
def smtp(request):
smtp = smtplib.SMTP("smtp.gmail.com")
def fin():
smtp.close()
request.addfinalizer(fin)
return smtp
def test_ehlo(smtp):
response, msg = smtp.ehlo()
assert response == 250
assert b"smtp.gmail.com" in msg
def test_noop(smtp):
response, msg = smtp.noop()
assert response == 250
Здесь фикстура делает следующее:
- Открывает smtp соединение
- Отдает объект smtp
- Хранит состояние возвращаемого объекта
- В конце теста аккуратно закрывает соединение
А в самом тесте, нам остается только указать в параметрах функции, какие фикстуры мы используем и py.test сам вызовет функцию с ней. Если внимательно посмотрите, то увидите, что наша функция smtp тоже принимает на вход параметр, который также, является системной фикстурой py.test.
Думаю, следует пояснить пункт №3. Т.к. фикстура объявлена со scope=”module”, то эта функция вызывается лишь 1 раз при импорте каждого модуля с тестами и каждая тест кейс внутри 1 модуля получает один и тот же объект smtp. А при завершении запуска тест-кейсов данного модуля, выполнится finalize функция smtp.close()
Механизм фикстур для новичков является большой магией, т.к. позволяет как сделать множетсов интересных вещей, так и нажить еще больше проблем с поиском причин багов.
Плагины
Плагинов для py.test написано уже множество, если судить по pip list, то их 227 на любой случай жизни.
Расширять py.test можно как с помощью отдельно написаных плагинов (таких как pytest-bdd, pytest-sugar), так и с помощью хуков, которые расположены непосредственно в ваших тестах.
В первом случае, py.test использует механизм setuptools entrypoint для поиска своих установленных плагинов и их загрузки
Во втором случае, он сканирует все дерево исходных кодов на предмет обнаружения hook-функций, которые вызываются на самые разнообразные действия движка начиная от инициализации (работа с передаными параметрами коммандной строки) и заканчивая выводом отчета и завершения теста.
Весь список хуков и их параметров можно посмотреть в исходных кодах, либо официальной документации.
Например, рассмотрим простой хук, который перед запуском тестов проверяет, есть ли в директории теста fabfile.py и при его нахождении запускает fab prepare, для подготовки окружения к тестам:
def pytest_pycollect_makemodule(path, parent):
fab_path = LocalPath(path.dirname).join('fabfile.py')
if fab_path.isfile():
subprocess.check_call(['/usr/local/bin/fab',
'-f', fab_path.strpath, 'prepare'],
stdout=subprocess.PIPE, stderr=subprocess.STDOUT)
Распределенный запуск тестов
py.test из коробки расчитан на запуск тестов распределенно, как многопоточно на данной машине, так и в разных процессах/интерпретаторах на других машинах, с помощью библиотеки execnet. Для запуска тестов распредленно свего лишь надо указать настройки через коммандную строку, например самый простой случай, в несколько потоков:
py.test -n 4 tests/
где n - количество потоков. py.test автоматически создаст 4 потока, распределит тесты по ним, соберет статистику и результаты, а по завершению выдаст отчет, как будто запускали в 1 поток. Точно также он работает и при запуске тестов через ssh на других серверах:
py.test --tx ssh=myhost//python=python2.5 --tx ssh=myhost//python=python2.6 tests/
Для первой статьи, думаю достаточно, в будущем постараюсь лучше раскрыть тему фикстур, а также плагинов.