Перейти к содержанию

172 уроков, 13 библиотек и челлендж «Что выведет код?» — бесплатно, код прямо в браузере

Начать обучение
Урок 1 из 20 Начальный 30 мин 100 XP

Первый тест на 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. Проверим молчание честно.

assert молчит, пока всё верно
def add(a, b):
    return a + b

assert add(2, 3) == 5
assert add(-1, 1) == 0

print("Обе проверки прошли молча: ни одной строки вывода")
Вывод
Обе проверки прошли молча: ни одной строки вывода

Ни точки, ни слова «ок» после каждой проверки — assert не болтает, а констатирует. Это свойство и делает его идеальным материалом для тестов: шум не печатается, а сбой невозможно пропустить. Посмотрим, как выглядит сбой.

проваленный 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
AssertionError
Блок не запускается: он намеренно вызывает ошибку. В настоящем traceback строки перед AssertionError будут другими, суть всегда в последней строке.
опечатка: = вместо ==
def add(a, b):
    return a + b

assert add(2, 2) = 5
Вывод
    assert add(2, 2) = 5
                     ^
SyntaxError: invalid syntax
Настоящий ответ Python 3.13 на такую строку. Файл даже не начинает выполняться: SyntaxError ловится до запуска. Строку с путём к файлу из вывода убрали — суть всегда в тексте ошибки.

Первый тест: функция test_add

Тест на pytest — это обычная функция, которая подчиняется двум правилам: имя начинается с test_ и внутри есть проверки. Никаких обязательных классов, наследований и импортов фреймворка для базового сценария — вот полный тестовый файл:

test_calc.py — тестовый файл целиком
def add(a, b):
    return a + b

def test_add():
    assert add(2, 3) == 5
Это содержимое файла, а не скрипт для запуска: у него нет print и нет вызовов — запускать его будет pytest. В песочнице ниже файл создаётся кодом автоматически.

Разбираем. 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 есть целый словарь кодов возврата, и пятёрка означает «собрал ноль тестов». Это не сбой фреймворка, а честный ответ: проверить было нечего. Запомни его — он часто всплывает после опечатки в имени.

терминал, не Python
pip install pytest
pytest -q
Вывод
.                                                                        [100%]
1 passed in 0.01s
Это команды терминала, поэтому блок не запускается в браузере. Настоящий вывод pytest -q из папки с тестами: та же точка и та же сводка, что и в песочнице.

Что дальше

Первый тест написан и прошёл: функция с именем 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))
Проверь себя
0 / 6

1. Какую функцию pytest считает тестом?

2. Что означает точка в строке отчёта pytest?

3. Какой код выхода возвращает pytest у полностью зелёного набора?

4. Что делает строка Path("test_calc.py").write_text(...) в песочнице?

5. Что напечатает print("exit:", int(rc)), если pytest не нашёл ни одного теста?

6. Зачем тесту оператор assert?

Карточки терминов
Запомнено: 0 / 6
Практика

Функция discount(price) возвращает цену со скидкой десять процентов без копеек: price - price // 10. Положи её и два теста в один файл test_discount.py: при price=1000 ждём 900, при price=250 ждём 225. Запусти тесты через pytest.main и напечатай код выхода.

practice.py
Вопросы и ответы по уроку

Как написать и запустить первый тест на 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-канал или свой блог — так о проекте узнают новые читатели.

TelegramVK

Похожие уроки по темам

Подобраны автоматически по пересечению тем и ключевых слов.