PyTest
Введение
Pytest поддерживает тесты, созданные с
unittest
.
Главное преимущество Pytest заключается в особенностях написания TestCase.
TestCase в pytest — это серия функций в файле Python,
которые начинаются с имени test_.
У Pytest есть и другие отличительные особенности:
- поддержка встроенного оператора assert (не нужно использовать специальные методы self.assert);
- поддержка фильтрации;
- возможность перезапуска, начиная с последнего неудачного теста;
- экосистема из сотен плагинов, расширяющих функциональность.
Написание теста TestSum в pytest выглядит так:
def test_sum(): assert sum([1, 2, 3]) == 6, "Should be 6" def test_sum_tuple(): assert sum((1, 2, 2)) == 6, "Should be 6"
Здесь удалены базовый класс TestCase и любое использование классов в принципе, а также точка входа с командной строки. Как обычно, дополнительная информация представлена на сайте Pytest.
Пример без IDE
Простейший пример в консоле
Linux
Нужно подготовить окружение и создать два файла - с кодом и тестом для этого кода.
Файл с кодом можно назвать как угодно - желательно с маленькой буквы и именем несовпадающим с зарезервированными.
Файл с тестом обычно называют также но с префиксом test_ или реже постфиксом _test
mkdir /home/$(whoami)/python/pytest/qa
cd /home/$(whoami)/python/pytest/qa
python -m venv venv
source venv/bin/activate
python -m pip install --upgrade pip
python -m pip install pytest
touch psum.py test_psum.py
В файле psum.py напишите простую функцию, например сложение
def add(x, y): return x + y
А в файле test_psum.py будет тест для этой функции
from psum import add def test_psum(): assert add(2, 3) == 5, "Should be 5"
Чтобы запустить тест нужно выполнить
pytest test_psum.py
=================== test session starts =================== platform linux -- Python 3.9.5, pytest-7.0.1, pluggy-1.0.0 rootdir: /home/andrei/python/pytest/qa collected 1 item test_psum.py . [100%] ========================= 1 passed in 0.01s =========================
Если функция вернёт результат отличный от ожидаемого, например, в случае ошибки в самом тесте
assert add(2, 3) == 999, "Should be 999"
pytest test_psum.py
======================== test session starts ========================= platform linux -- Python 3.9.5, pytest-7.0.1, pluggy-1.0.0 rootdir: /home/andrei/python/pytest/qa collected 1 item test_psum.py F [100%] ============================== FAILURES ============================== _____________________________ test_psum ______________________________ def test_psum(): > assert add(2, 3) == 999, "Should be 999" E AssertionError: Should be 999 E assert 5 == 999 E + where 5 = add(2, 3) test_psum.py:4: AssertionError ======================= short test summary info ====================== FAILED test_psum.py::test_psum - AssertionError: Should be 999 ========================= 1 failed in 0.02s ==========================
Структура проекта с тестами на PyTest
Как только проект перерастает микроскопический размер держать тесты в одной директории с кодом становится неудобно.
Предположим, что код проекта лежит в директории app.
Типичным решением будет создание файла с тестом
test_psum.py
в поддиректорию tests
app ├── psum.py ├── tests │ └── test_psum.py └── venv ├── bin ├── include ├── lib ├── lib64 -> lib └── pyvenv.cfg
Если теперь запустить тест из директории app то pytest поймёт как импортировать psum
python -m pytest tests/test_psum.py
Если запустить этот же тест прямо из директории tests - появится ошибка ImportError while importing test module
Тестирование проверки аргументов на тип
Демонстрацю применения PyTest часто начинают с функций сложения или умножения.
Убедимся, что находимся в директрии app и создадим ещё два файла
prod.py
,
tests/test_prod.py
touch prod.py tests/test_prod.py
Теперь структура проекта имеет вид:
app ├── prod.py ├── psum.py ├── __pycache__ │ ├── psum.cpython-39.pyc │ └── test_psum.cpython-39-pytest-7.0.1.pyc ├── tests │ ├── __pycache__ │ ├── test_prod.py │ └── test_psum.py └── venv ├── bin ├── include ├── lib ├── lib64 -> lib └── pyvenv.cfg
Добавим в эти файлы следующий код:
# Code def prod(a, b): return a * b
# Test from prod import prod def test_prod(): res = prod(3, 4) assert res == 12
python -m pytest tests/test_prod.py
Эта функция подразумевает использование чисел. Но валидации аргументов нет.
С помощью
добавим валидацию и напишем тест.
# prod.py def prod(a, b): if not all( map(lambda p: isinstance(p, (int, float)), (a, b)) ): raise TypeError("Not valid argument data type") print("prod.py: Valid arguments") return a * b
# test_prod.py import pytest from prod import prod def test_prod(): res = prod(3, 4) assert res == 12 def test_arguments(): try: # Заведомо неправильный тип данных должен быть пойман # "" это строка а не число res = prod("", 4) # Если использовать валидные аргументы исключение не поднимется # и тест упадёт # res = prod(1, 2) except TypeError as err: # if TypeError is caught pass else: print("test_prod.py: Invalid argument is not caught") pytest.fail() # assert False
Вместо pytest.fail() можно использовать assert False, тогде и импортировать pytest необязательно
python -m pytest -v tests/test_prod.py
================================ test session starts =============================== platform linux -- Python 3.9.5, pytest-7.0.1, pluggy-1.0.0 -- /home/andrei/python/pytest/otus/venv/bin/python cachedir: .pytest_cache rootdir: /home/andrei/python/pytest/app collected 2 items tests/test_prod.py::test_prod PASSED [ 50%] tests/test_prod.py::test_arguments PASSED [100%] ================================= 2 passed in 0.01s ================================
Если тест сломается PyTest выдаст следующий результат
================================ test session starts ================================ platform linux -- Python 3.9.5, pytest-7.0.1, pluggy-1.0.0 -- /home/andrei/python/pytest/otus/venv/bin/python cachedir: .pytest_cache rootdir: /home/andrei/python/pytest/app collected 2 items tests/test_prod.py::test_prod PASSED [ 50%] tests/test_prod.py::test_arguments FAILED [100%] ===================================== FAILURES ====================================== __________________________________ test_arguments ___________________________________ def test_arguments(): try: # "" is a string and should not be accepted res = prod(3, 4) except TypeError as err: # if TypeError is caught pass else: print("test_prod.py: Invalid argument is not caught") > pytest.fail() E Failed tests/test_prod.py:19: Failed ------------------------------- Captured stdout call -------------------------------- prod.py: Valid arguments test_prod.py: Invalid argument is not caught ============================== short test summary info ============================== FAILED tests/test_prod.py::test_arguments - Failed ============================ 1 failed, 1 passed in 0.02s ============================
unittest из PyTest
Если на проекте уже есть тесты, созданные на
unittest
можно их не переписывать - PyTest поймёт синтаксис unittest
Рассмотрим
проверку решения квадратного уравнения на unittest
Если запустить эти же тесты с помощью PyTest
python -m pytest -v tests/test_quadratic.py
==================== test session starts ===================== platform linux -- Python 3.9.5, pytest-7.0.1, pluggy-1.0.0 -- /home/andrei/python/unittest/app/venv/bin/python cachedir: .pytest_cache rootdir: /home/andrei/python/unittest/app collected 5 items tests/test_quadratic.py::TestQuadratic::test_raises_type_error PASSED [ 20%] tests/test_quadratic.py::TestQuadratic::test_result_is_tuple PASSED [ 40%] tests/test_quadratic.py::TestQuadratic::test_single_root PASSED [ 60%] tests/test_quadratic.py::TestQuadratic::test_two_roots PASSED [ 80%] tests/test_quadratic.py::TestQuadratic::test_zero_a_and_b PASSED [100%] ======================= 5 passed in 0.01s ====================
--last-failed: перезапуск только упавших тестов
Если какой-то тест упал по причине ошибки в самом тесте, нужно исправить его и перезапустить.
Чтобы не перезапускать все тесты можно воспользоваться опцией --last-failed
Предположим в test_prod.py допущена ошибка
def test_prod(): res = prod(3, 4) assert res == 120
python -m pytest test_prod.py
==================== test session starts ==================== platform linux -- Python 3.9.5, pytest-7.1.1, pluggy-1.0.0 rootdir: /home/andrei/github/pytest1/app collected 2 items tests/test_prod.py F. [100%] ========================== FAILURES ========================= _________________________ test_prod _________________________ def test_prod(): res = prod(3, 4) > assert res == 120 E assert 12 == 120 tests/test_prod.py:8: AssertionError ------------------- Captured stdout call -------------------- prod.py: Valid arguments ================== short test summary info ================== FAILED tests/test_prod.py::test_prod - assert 12 == 120 ================ 1 failed, 1 passed in 0.02s ================ (venv) andrei@LL-andrei2 {12:04}~/github/pytest1/app:master ✗
После исправления ошибки можно перезапустить только упавший тест
python -m pytest --last-failed tests/test_prod.py
=============================== test session starts ============================== platform linux -- Python 3.9.5, pytest-7.1.1, pluggy-1.0.0 rootdir: /home/andrei/github/pytest1/app collected 2 items / 1 deselected / 1 selected run-last-failure: rerun previous 1 failure tests/test_prod.py . [100%] ======================== 1 passed, 1 deselected in 0.01s ========================= (venv) andrei@LL-andrei2 {12:06}~/github/pytest1/app:master ✗
В PyCharm есть специальная кнопка Rerun Failed Tests - в виде зелёного треугольника и красного круга с восклицательным знаком внутри.

--capture=no: не скрывать вывод
Если в ваших тестах есть какие-то вызовы print() при обычном запуске pytest их не будет видно
def test_prod(): print("testing prod") res = prod(3, 4) assert res == 120
Чтобы увидеть вывод print() нужно использовать опцию
--capture=no
python -m pytest -v --capture=no test_prod.py
======================== test session starts ========================== platform linux -- Python 3.9.5, pytest-7.1.1, pluggy-1.0.0 -- /home/andrei/github/pytest1/venv/bin/python cachedir: .pytest_cache rootdir: /home/andrei/github/pytest1/app collected 2 items tests/test_prod.py::test_prod testing prod prod.py: Valid arguments PASSED tests/test_prod.py::test_arguments PASSED ========================= 2 passed in 0.01s ==========================
Тестирование решения квадратного уравнения
Предположим мы решаем квадратное уравнение следующим скриптом.
from math import sqrt TYPE_ERROR_TEXT = "Not valid argument type" def quadratic_solve(a, b, c): if not all( map( lambda p: isinstance(p, (int, float)), (a, b, c) ) ): raise TypeError(TYPE_ERROR_TEXT) print("Types are OK") if a == 0: if b == 0: # a и b 0: решения нет return None, None return -c / b, None d = b ** 2 - 4 * a * c if d < 0: return None, None d_root = sqrt(d) divider = 2 * a x1 = (-b + d_root) / divider x2 = (-b - d_root) / divider if d == 0: x2 = None elif x2 > x1: x1, x2 = x2, x1 return x1, x2 if __name__ == "__main__": print(quadratic_solve(1, -1, -2)) print(quadratic_solve("", 2, 3))
Можно решать и по-другому, главное что нужно для теста - проверка аргументов и возвращение корней в виде кортежа, где отсутствие корня передаётся как None.
Напишем тест, который проверяет что аргументы это числа, а вернуля кортеж
# test_quadratic.py import pytest from quadratic import quadratic_solve, TYPE_ERROR_TEXT class TestQuadratic: def test_raises_type_error(self): with pytest.raises(TypeError) as exc_info: quadratic_solve(1, "", 2) assert str(exc_info.value) == TYPE_ERROR_TEXT def test_result_is_tuple(self): res = quadratic_solve(0, 0, 0) assert isinstance(res, tuple)
Теперь нужно проверить как-минимум три варианта: когда есть оба корня, когда корень один, когда нет корней.
Можно написать три отдельных теста как это было сделано на
unittest
но удобнее применить встроенную в PyTest
параметризацию тестов
.
-k: запуск определённого теста
Если в файле с тестами больше одного теста, может возникнуть необходимость запустить только часть
Чтобы запустить тест по его названию нужно воспользоваться опцией -k
python -m pytest -v -k "test_basic_param_prod" tests/test_prod.py
================================================= test session starts ================================================== platform linux -- Python 3.9.5, pytest-7.1.1, pluggy-1.0.0 -- /home/andrei/github/pytest1/venv/bin/python cachedir: .pytest_cache rootdir: /home/andrei/github/pytest1/app collected 18 items / 15 deselected / 3 selected tests/test_prod.py::test_basic_param_prod[args0-0] PASSED [ 33%] tests/test_prod.py::test_basic_param_prod[args1-0] PASSED [ 66%] tests/test_prod.py::test_basic_param_prod[args2-132] PASSED [100%] =========================================== 3 passed, 15 deselected in 0.01s ===========================================
Добавить PyTest в Pycharm
Чтобы добавить PyTest в PyCharm воспользуйтесь следующей инструкцией
Settings (Ctrl + Alt + S) → Tools → Python Integrated Tools → Default test runner:
Выбрать pytest. Если он ещё не был добавлен появится предупреждение и кнопка Fix.

Нужно нажать кнопку Fix
Внизу главного экрана настроек появится сообщение
Installing package 'pytest'
Дождитесь когда оно сменится сообщением об успешной установке и с экрана исчезнет предупреждение.

При успешной конфигурации PyCharm у каждого теста слева появится зелёный треугольник, нажав на который
можно будет запусть данный тест.
Если тесты находятся в отдельной директории, возможно вам придётся вручную указать директорию с
проектом как источник для тестов.
Делается это следующим образом
Правый клик на директорию → Mark Directory as → Test Sources Root
Автор статьи: Андрей Олегович
Тестирование | |
Параметризация | |
Ошибки | |
Видео |