Допустим, разрабатывается некоторый проект. К проекту требуется написать интеграционные и системные тесты, а также, возможно, нагрузочные и еще какие-то. Для решения этой задачи Python подходит просто идеально. В чем мы с вами скоро и убедимся, познакомившись с фреймворком PyTest и некоторыми плагинами к нему.
Установка:
Чтобы было что тестировать, создадим незамысловатый модуль fibgen, вся реализация которого приведена ниже:
«»»
Generates Fibonacci numbers
:param num: how many numbers to generate
:return: generator of first num Fibonacci numbers
>>> list(fibgen(10))
[1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
«»»
if type ( num ) is not int :
raise TypeError ( «type of num argument should be int» )
if num <= 0 :
return
x1 = 0
x2 = 1
i = 1
yield 1
while i < num:
i = i + 1
res = x1 + x2
x1 = x2
x2 = res
yield res
if __name__ == «__main__» :
print ( list ( fibgen ( 10 ) ) )
Файл tests/test_fibgen.py:
import pytest
class TestFibgen:
def test_type_error ( self ) :
with pytest. raises ( TypeError ) :
list ( fibgen ( ‘ololo’ ) )
def test_negative ( self ) :
assert ( list ( fibgen ( — 1 ) ) == [ ] )
def test_empty ( self ) :
assert ( list ( fibgen ( 0 ) ) == [ ] )
def test_one ( self ) :
assert ( list ( fibgen ( 1 ) ) == [ 1 ] )
def test_two ( self ) :
assert ( list ( fibgen ( 2 ) ) == [ 1 , 1 ] )
def test_three ( self ) :
assert ( list ( fibgen ( 3 ) ) == [ 1 , 1 , 2 ] )
def test_seven ( self ) :
# result = list(fibgen(10))
result = list ( fibgen ( 7 ) )
expected = [ 1 , 1 , 2 , 3 , 5 , 8 , 13 ]
assert ( result == expected )
@ pytest. mark . randomize ( num = int , min_num = 3 , max_num = 1000 , ncalls = 99 )
def test_quickcheck ( self , num ) :
result = list ( fibgen ( num ) )
assert ( result [ 0 ] < result [ — 1 ] )
assert ( len ( result ) == num )
Чтобы было несколько тестовых наборов, создадим еще tests/subdir/test_math.py:
def setup_class ( self ) :
print ( » n === TestMath — setup class === n » )
def teardown_class ( self ) :
print ( » n === TestMath — teardown class === n » )
def setup ( self ) :
print ( «TestMath — setup method» )
def teardown ( self ) :
print ( «TestMath — teardown method» )
def test_add ( self ) :
assert ( 2 + 2 == 4 )
def test_mul ( self ) :
assert ( 3 * 3 == 9 )
Теперь попробуем разобраться, что же мы здесь видим.
Фреймворк PyTest находит тесты, глядя на имена файлов, классов и методов. Файлы с тестами должны содержать в имени строку «test_», имена классов — начинаться с «Test», а методов — со строки «test_». Все проверки почти всегда выполняются при помощи assert, то есть, никаких навороченных DSL, как в ScalaTest , не предусмотрено. Если требуется проверить, что код бросает некое исключение, можно использовать конструкцию with pytest. raises
, как в тесте test_type_error.
Вдумчивые читатели могли заподозрить property-based тест в коде:
@ pytest. mark . randomize ( num = int , min_num = 3 , max_num = 1000 , ncalls = 99 )
def test_quickcheck ( self , num ) :
result = list ( fibgen ( num ) )
assert ( result [ 0 ] < result [ — 1 ] )
assert ( len ( result ) == num )
# …
Все правильно, это он и есть. Аргумент num будет генерироваться случайным образом из диапазона 3..1000, всего же тест будет запущен 99 раз. Следует однако иметь в виду, что property-based тесты в PyTest намного тупее, нежели в QuickCheck или ScalaCheck. Так, к примеру, никакой гарантированной проверки граничных случаев нет, тест просто выполняется с какими-то случайными значениями. Кроме того, shrinking не поддерживается.
Следующий отрывок кода говорит сам за себя (так называемые «фикстуры»):
def setup_class ( self ) :
print ( » n === TestMath — setup class === n » )
def teardown_class ( self ) :
print ( » n === TestMath — teardown class === n » )
def setup ( self ) :
print ( «TestMath — setup method» )
def teardown ( self ) :
print ( «TestMath — teardown method» )
# …
Метод с именем setup выполняется перед каждыйм тестом, а с именем teardown, вполне закономерно — после. Аналогично метод setup_class выполняется перед началом выполнения всех тестов в классе. После того, как все тесты в классе были выполнены, вызывается teardown_class.
Самый простой способ запустить все тесты — воспользоваться PyCharm :
Запуск всех тестов из консоли:
Переменная окружения PYTHONPATH нужна для того, чтобы PyTest нашел пакет fibgen. Если вы пишите интеграционные или системные тесты, они вполне могут опираться на модули, объявленные в текущем каталоге. Так что, эту переменную лучше всегда выставлять, чтобы не наткнуться на ошибку вроде ImportError: No module named fibgen
.
Желательно всегда использовать verbose режим, чтобы видеть, какие тесты были запущены, видеть полностью какие assert’ы и почему провалились, и так далее:
Флаг -l
включает отображение значений переменных при падении теста:
Запуск конкретного набора тестов, короткая версия:
PYTHONPATH =. py.test -v -l -k TestFibgen
Запуск тестов из конкретного файла:
Из конкретного класса:
Запуск конкретного метода:
tests / subdir / test_math.py::TestMath::test_add
Можно указывать несколько тестов:
tests / subdir / test_math.py::TestMath::test_add
tests / test_fibgen.py::TestFibgen::test_empty
Тестирование примеров кода из документации (doctest):
Определение степени покрытия модуля fibgen тестами с выводом номеров непокрытых строк в терминал и генерацией отчета о покрытии в HTML:
—cov-report html
Кстати, оказывается, что в PyCharm Community Edition нет поддержки code coverage , этот функционал есть только в Professional Edition.
C генерацией junit.xml:
С генерацией HTML:
К сожалению, приведенная команда генерирует один большой HTML-файл на все наборы тестов. Решить эту проблему можно командой:
-exec py.test -v -l { } —html =html / { } .html ;
Но, увы, в этом случае мы потеряем коды возврата, что плохо работает с системами непрерывной интеграции вроде Jenkins .
Поэтому я в итоге написал такую обвязку:
import os
import re
import subprocess
html_prefix = «./html/»
idx_html = «<table> n <tr><td>test</td><td>status</td></tr> n »
for root , dirs , files in os . walk ( «tests» ) :
for fname in files:
if re . search ( » \ .py$» , fname ) is not None :
sname = os . path . join ( root , fname )
html = html_prefix + sname + «.html»
cmd = «PYTHONPATH=. py.test —doctest-modules -v -l » +
sname + » —html=» + html
code = subprocess . call ( cmd , shell = True )
status = «<p style=’color:green;’>Success</p>»
if code != 0 :
status = «<p style=’color:red;’>FAILURE</p>»
idx_html = idx_html + «<tr><td><a href='» + sname +
«.html’>» + sname + «</a></td><td>» + status +
«</td></tr> n »
idx_html = idx_html + «</table> n »
with open ( html_prefix + «index.html» , «w» ) as f:
f. write ( idx_html )
Напоследок, как обычно, немного ссылок по теме:
- Официальный сайт PyTest ;
- Модули на PyPI: pytest-quickcheck , pytest-html , pytest-cov ;
- Еще больше примеров использования фикстур (fixtures) в PyTest ;
- Тут чуть больше подробностей про конструкцию pytest.raises ;
- Allure — говорят, умеет делать красивые репорты из junit.xml;
- Полная версия исходников к этому посту на GitHub ;
А используете ли вы PyTest и что, собственно, тестируете с его помощью?
Дополнение: Пример использования PyTest для тестирования проектов на C/C++ вы найдете в посте, посвященном CMake .