Первый тест на pytest: assert, запуск и первый отчёт
Первая тест-функция на обычном assert: pytest сам находит её, запускает и печатает отчёт — точка, 1 passed и код выхода 0. Всё прямо в браузере.
Редакция Питоники
Функция написана, и надо убедиться, что она работает. Знакомый ритуал: в конец файла лепится print, запускаешь скрипт, глазами сверяешь вывод с тем, что в голове. Одна функция — один print, и всё терпимо. Пять функций — пять print'ов, вывод перемешивается, и сверять уже утомительно. А пятьдесят функций — это стена текста, в которой любая ошибка тонет так же надёжно, как тонула бы без единого print.
Автотесты — это когда сверку с ожиданием берёт на себя программа. Ты один раз записываешь, что должна вернуть функция, а дальше при каждом запуске машина отвечает коротким вердиктом: работает или сломалось. Сегодня пишем первый такой тест на pytest — самом популярном тест-фреймворке Python — и научимся читать его первый отчёт.
Почему print-отладка не масштабируется
Ручная проверка устроена одинаково всегда: запустил код, посмотрел на печать, сам решил, совпало ли ожидание с реальностью. Посмотрим на этот ритуал вблизи — на примере функции сложения из будущего тестового файла.
def add(a, b):
return a + b
print(add(2, 3)) # ждём 5
print(add(-1, 1)) # ждём 0
print(add(0, 0)) # ждём 0
У этого подхода три системные беды, и все три растут с размером проекта:
- вывод растёт вместе с кодом: пятьдесят функций — пятьдесят печатных строк, и ошибка теряется в потоке;
- сверка на человеке: правильное значение надо помнить и каждый раз сравнивать вручную — устал, отвлёкся, пропустил;
- вердикта нет: по пачке print'ов не видно общий статус — всё зелёное или что-то уже сломалось.
Автотест меняет роли: человек один раз записывает ожидание, а сверкой занимается машина при каждом запуске. Инструмент, который делает это удобно, — pytest.
assert: проверка, которая молчит
В сердце теста лежит обычный оператор Python — assert. Он принимает условие: если условие истинно, assert молчит, а если ложно — роняет программу с исключением AssertionError. Проверим молчание честно.
def add(a, b):
return a + b
assert add(2, 3) == 5
assert add(-1, 1) == 0
print("Обе проверки прошли молча: ни одной строки вывода")
Обе проверки прошли молча: ни одной строки вывода
Ни точки, ни слова «ок» после каждой проверки — assert не болтает, а констатирует. Это свойство и делает его идеальным материалом для тестов: шум не печатается, а сбой невозможно пропустить. Посмотрим, как выглядит сбой.
def add(a, b):
return a + b
assert add(2, 2) == 5
Traceback (most recent call last):
File "check.py", line 5, in <module>
assert add(2, 2) == 5
AssertionErrordef add(a, b):
return a + b
assert add(2, 2) = 5
assert add(2, 2) = 5
^
SyntaxError: invalid syntaxПервый тест: функция test_add
Тест на pytest — это обычная функция, которая подчиняется двум правилам: имя начинается с test_ и внутри есть проверки. Никаких обязательных классов, наследований и импортов фреймворка для базового сценария — вот полный тестовый файл:
def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5
Разбираем. add — та самая функция, которую проверяем; здесь она лежит в одном файле с тестом для простоты, а в настоящем проекте тест импортирует её из модуля. test_add — тест: имя начинается с test_, внутри одна проверка через assert. Больше ничего не нужно: ни регистрации, ни списка тестов — pytest найдёт функцию по имени сам.
Как его запустить? Дома — командой pytest в терминале из папки проекта. На этой странице работает песочница: код исполняется в браузерном интерпретаторе, и тестовый файл создаёт сам код блока, а запускать тесты вызывает pytest программно.
from pathlib import Path
Path("test_calc.py").write_text("""def add(a, b):
return a + b
def test_add():
assert add(2, 3) == 5
""", encoding="utf-8")
import pytest
rc = pytest.main(["test_calc.py", "-q", "--no-header", "-p", "no:cacheprovider"])
print("exit:", int(rc))
. [100%] 1 passed in 0.02s exit: 0
Разбираем построчно, потому что этот паттерн будет в каждом запускаемом блоке курса. Path("test_calc.py").write_text(...) — пишет тестовый файл на диск песочницы: у блока нет рук, чтобы создать файл в редакторе, поэтому он создаёт его кодом. import pytest — подключает фреймворк; в песочнице он уже установлен. pytest.main([...]) — программный запуск: список строк внутри — те же аргументы, что ты дал бы команде pytest в терминале. -q просит краткий отчёт, --no-header убирает шапку, -p no:cacheprovider — не создавать кэш-папку. Результат возвращается кодом, и print("exit:", int(rc)) печатает его последней строкой.
| Элемент отчёта | Что означает |
|---|---|
| . | точка — один тест прошёл; два теста — две точки, три — три |
| [100%] | прогресс: выполнены все собранные тесты набора |
| 1 passed | сводка: один тест прошёл, время прогона после слов in |
| exit: 0 | код выхода: ноль — зелёный набор, любое другое число — проблемы |
Код выхода — деталь, которая кажется мелочью, пока не встретишься с автоматизацией. Сервер непрерывной сборки, редактор и командные скрипты не читают человеческий отчёт — они смотрят на код возврата: ноль значит «всё зелёное, заливай», единица значит «стоп, сломалось». pytest ставит этот код автоматически, тебе ничего дописывать не нужно.
Тихий случай: тестов нет
А что увидит pytest, если в файле нет ни одной функции с именем test_? Проверим честно: положим рядом файл calc.py с обычной функцией и укажем его pytest'у.
from pathlib import Path
Path("calc.py").write_text("""def add(a, b):
return a + b
""", encoding="utf-8")
import pytest
rc = pytest.main(["calc.py", "-q", "--no-header", "-p", "no:cacheprovider"])
print("exit:", int(rc))
no tests ran in 0.01s exit: 5
Ни одной точки, сообщение no tests ran — и код выхода 5, а не ноль. У pytest есть целый словарь кодов возврата, и пятёрка означает «собрал ноль тестов». Это не сбой фреймворка, а честный ответ: проверить было нечего. Запомни его — он часто всплывает после опечатки в имени.
pip install pytest
pytest -q
. [100%] 1 passed in 0.01s
Что дальше
Первый тест написан и прошёл: функция с именем test_, внутри assert, запуск через pytest.main — и понятный отчёт с точкой, сводкой и кодом выхода. Дальше по курсу: конвенции имён — какие файлы и функции pytest считает тестами и как выполнить один тест из файла; разбор падения — что pytest показывает, когда ожидание не совпало с реальностью. С каждым уроком набор растёт, а ручной пересчёт print'ов остаётся в прошлом.
Точка в отчёте pytest — это проверка, которую выполнила машина: молчаливый assert сказал больше, чем стена print'ов.
Сначала предскажи ответ в голове — это главный навык программиста.
def mul(a, b):
return a * b
assert mul(3, 4) == 12
print("проверка пройдена")
results = [True, True, False]
passed = sum(results)
print(str(passed) + " passed, " + str(len(results) - passed) + " failed")
names = ["check_add", "test_add", "test_sub"]
tests = [n for n in names if n.startswith("test_")]
print(len(tests))
1. Какую функцию pytest считает тестом?
2. Что означает точка в строке отчёта pytest?
3. Какой код выхода возвращает pytest у полностью зелёного набора?
4. Что делает строка Path("test_calc.py").write_text(...) в песочнице?
5. Что напечатает print("exit:", int(rc)), если pytest не нашёл ни одного теста?
6. Зачем тесту оператор assert?
Функция discount(price) возвращает цену со скидкой десять процентов без копеек: price - price // 10. Положи её и два теста в один файл test_discount.py: при price=1000 ждём 900, при price=250 ждём 225. Запусти тесты через pytest.main и напечатай код выхода.
Как написать и запустить первый тест на pytest?
Создай файл с именем test_calc.py, положи в него обычную функцию с именем test_ и проверкой через assert — например, def test_add(): assert add(2, 3) == 5. Запусти командой pytest из папки проекта: pytest сам найдёт функцию, выполнит её и напечатает отчёт. На этой странице то же самое можно сделать в браузере: код-блок создаёт файл и вызывает pytest.main.
Почему pytest не видит мой тест и пишет no tests ran?
Почти всегда дело в имени. pytest считает тестом только функции, чьё имя начинается с test_, в файлах, чьё имя начинается с test_. Функция check_add или файл calc.py для него не существуют: отчёт будет зелёным, а тестов — ноль, и код выхода будет 5. Переименуй с префиксом test_ — тест соберётся.
Что означает строка 1 passed в отчёте pytest?
Это сводка прогона: один тест прошёл, ноль упал, а после слова in указано время выполнения. Слева на той же строке печатается расшифровка по буквам: точка за каждый пройденный тест, F за упавший, E за ошибку запуска. Код выхода 0 под строкой подтверждает зелёный набор.
Нужно ли знать unittest, чтобы работать с pytest?
Нет. Для базовых тестов pytest не требует ни классов, ни наследования от TestCase, ни методов assertEqual — достаточно обычных функций и оператора assert. При этом pytest умеет запускать и старые unittest-тесты, так что миграция старого проекта возможна постепенно, без переписывания всего набора разом.
Понравился урок? Сошлитесь на него
«Точка в отчёте pytest — это проверка, которую выполнила машина: молчаливый assert сказал больше, чем стена print'ов.»
Скопируйте готовую ссылку в формате HTML, Markdown или чистый адрес и вставьте в статью на Habr, VC, Telegram-канал или свой блог — так о проекте узнают новые читатели.
Что читать дальше
pytest · Урок 2
Как pytest находит тесты: конвенции имён файлов и функций
pytest не читает мысли: тестом становится только то, что названо по конвенции — test_*.py, test_-функции, классы Test*. И адрес одного теста: файл::тест.
pytest · Урок 3
Отчёт pytest: подробный вывод -v и разбор падения
Зелёная точка — скучный отчёт, и это хорошо. Настоящая сила pytest раскрывается при падении: он показывает строку, ожидание, реальность и разницу между ними.
Похожие уроки по темам
Подобраны автоматически по пересечению тем и ключевых слов.
Flask · Урок 15
Тестирование Flask: pytest и test_client
Пятнадцатый урок курса Flask — мост в мир автотестов: проверки API из предыдущих уроков становятся тест-функциями pytest с assert, а app.test_client() работает внутри теста без сервера и сети.
flask тестирование pytest test_clientтестирование flask api pytest
requests · Урок 1
Библиотека requests Python с нуля: первый GET-запрос
Первый GET-запрос на requests: что улетает по сети, что возвращается и как разобрать ответ на статус, заголовки и тело — механику проверяем прямо в браузере.
requests python с нуляпервый get запрос python
pytest · Урок 5
Фикстуры pytest: @pytest.fixture для подготовки данных
Одна и та же корзина собирается в каждом тесте заново — знакомая копипаста. Фикстура описывает подготовку один раз, а pytest сам передаёт результат в тест по имени.
pytest фикстуры fixturepytest.fixture примеры