Блог gigimon'а

Немного про py.test

Последнее время я все чаще работаю с популярной библиотекой для тестирования - py.test. В компании, где я работаю, мы полностью перешли на ее использование в написании, как unit тестов, так и интеграционных.

Чем “крут” py.test?

Из главных фишек py.test можно выделить следующие:

  1. Раскрытие asssert’ов - при выводе ошибки, он красиво показывает, что и с чем мы сравнили, выводит типы и доходчиво это выводит, пряча весь traceback, который привел к assert’у, а также пытаясь самостоятельно вывести уточняющий текст.
  2. Система фикстур - специальные функции, которые неявно передаются в тестируемую функцию. Является очень мощным инструментом, т.к. позволяет делать глобальные/локальные setup/teardown вещи, легко переносимые между тестами.
  3. Система плагинов построеная на хуках, с помощью которой очень просто писать дополнительные плагины. В данный момент, есть плагины реализующие практически все, что угодно (от дополнительных assert’ов и выводилок на экран, до мощных инструментов, таких как bdd, coverage и т.п.)
  4. Возможность распределенного запуска тестов в автоматическом режиме и на разных машинах

Раскрытие 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

Здесь фикстура делает следующее:

  1. Открывает smtp соединение
  2. Отдает объект smtp
  3. Хранит состояние возвращаемого объекта
  4. В конце теста аккуратно закрывает соединение

А в самом тесте, нам остается только указать в параметрах функции, какие фикстуры мы используем и 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/

Для первой статьи, думаю достаточно, в будущем постараюсь лучше раскрыть тему фикстур, а также плагинов.

2008 — 2014