PyTest

Содержание
Введение
Пример применения
Структура проекта
Тестирование проверки аргументов на тип
Запуск unittest тестов
--last-failed: перезапуск только упавших тестов
--capture=no: не скрывать вывод
Тестирование решения квадратного уравнения
-k: запуск определённых тестов
parametrize: параметризация
Добавить PyTest в PyCharm
Похожие статьи

Введение

Pytest поддерживает тесты, созданные с unittest .

Главное преимущество Pytest заключается в особенностях написания TestCase.

TestCase в pytest — это серия функций в файле Python, которые начинаются с имени test_.

У Pytest есть и другие отличительные особенности:

Написание теста 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 - в виде зелёного треугольника и красного круга с восклицательным знаком внутри.

Rerun Failed Tests PyTest в PyCharm изображение с сайта www.andreyolegovich.ru
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.mark.parametrize( "args, expected_result", [ ((0, 0), 0), ((3, 0), 0), ((-11, -12), 132), ]) def test_basic_param_prod(args, expected_result): res = prod(*args) assert res == expected_result

Для более наглядной демонстрации результатов можно дать каждому тесту название, с помощью pytest.param id=

@pytest.mark.parametrize( "args, expected_result", [ pytest.param((0, 0), 0, id="zero - zero"), pytest.param((7, -8), -56, id="positive - negative"), pytest.param((13.0, 14), 182, id="float positive - positive"), ]) def test_param_prod(args, expected_result): res = prod(*args) assert res == expected_result

Простейший вариант параметризованного теста решения квадратного уравнения будет выглядеть следующим образом:

# Параметризация @pytest.mark.parametrize("args, expected_result",[ ((1, -3, -4), (4, -1)), ((0, 0, 0), (None, None)) ])

Удобнее дать каждому тесту название, с помощью pytest.param id=

# Параметризация с именованными наборами данных @pytest.mark.parametrize("args, expected_result",[ pytest.param((1, -3, -4), (4, -1), id="two roots", ), pytest.param((1, -2, 1), (1.0, None), id="single root", ), pytest.param((0, 0, 0), (None, None), id="no roots",) ]) def test_solution(self, args, expected_result): res = quadratic_solve(*args) assert res == expected_result

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/.pyenv/versions/3.9.5/bin/python cachedir: .pytest_cache rootdir: /home/andrei/github/pytest1/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_solution[two roots] PASSED [ 60%] tests/test_quadratic.py::TestQuadratic::test_solution[single root] PASSED [ 80%] tests/test_quadratic.py::TestQuadratic::test_solution[no roots] PASSED [100%] ========================================================== 5 passed in 0.01s =========================================================== andrei@LL-andrei2 {12:42}~/github/pytest1/app:master

Как можно увидеть при использовании опции -v все три теста [two roots], [single root], [no roots] успешно пройдены.

Добавить PyTest в Pycharm

Чтобы добавить PyTest в PyCharm воспользуйтесь следующей инструкцией

Settings (Ctrl + Alt + S) → Tools → Python Integrated Tools → Default test runner:

Выбрать pytest. Если он ещё не был добавлен появится предупреждение и кнопка Fix.

Добавить PyTest в PyCharm изображение с сайта www.andreyolegovich.ru
PyTest не найден

Нужно нажать кнопку Fix

Внизу главного экрана настроек появится сообщение

Installing package 'pytest'

Дождитесь когда оно сменится сообщением об успешной установке и с экрана исчезнет предупреждение.

Добавить PyTest в PyCharm изображение с сайта www.andreyolegovich.ru
PyTest как default test runner

При успешной конфигурации PyCharm у каждого теста слева появится зелёный треугольник, нажав на который можно будет запусть данный тест.

Если тесты находятся в отдельной директории, возможно вам придётся вручную указать директорию с проектом как источник для тестов.

Делается это следующим образом

Правый клик на директорию → Mark Directory as → Test Sources Root

Похожие статьи
Тестирование
Ошибки
Видео

Поиск по сайту

Подпишитесь на Telegram канал @aofeed чтобы следить за выходом новых статей и обновлением старых

Перейти на канал

@aofeed

Задать вопрос в Телеграм-группе

@aofeedchat

Контакты и сотрудничество:
Рекомендую наш хостинг beget.ru
Пишите на info@urn.su если Вы:
1. Хотите написать статью для нашего сайта или перевести статью на свой родной язык.
2. Хотите разместить на сайте рекламу, подходящую по тематике.
3. Реклама на моём сайте имеет максимальный уровень цензуры. Если Вы увидели рекламный блок недопустимый для просмотра детьми школьного возраста, вызывающий шок или вводящий в заблуждение - пожалуйста свяжитесь с нами по электронной почте
4. Нашли на сайте ошибку, неточности, баг и т.д. ... .......
5. Статьи можно расшарить в соцсетях, нажав на иконку сети: