Pytest: основы
Написал заведомо неверную проверку и запустил. Вывод: «assert 900.0 == 901.0, где 900.0 = discount(1000, 10)». Обычный оператор сравнения, никаких специальных методов - а в отчёте видно и результат, и то, из какого вызова он получился. Это и есть главное отличие pytest от классических библиотек тестирования.
Стержень: тест - это обычная функция с обычным assert; всё остальное pytest достраивает сам, разбирая ваше выражение.
// Формулировки: «почему pytest, а не unittest?», «как параметризовать тест?», «как проверить, что бросается исключение?»
Обычные функции и разбор проверок
Тест в pytest - это функция, имя которой начинается с test. Ни классов, ни наследования, ни специальных методов сравнения. Проверка пишется обычным оператором, а pytest переписывает его перед запуском так, чтобы при падении показать промежуточные значения. Мой замер это и показывает: в отчёте оказались и полученное значение, и вызов, который его дал.
Отсюда практический вывод: чем проще выражение в проверке, тем понятнее отчёт. Одна проверка на одну мысль, без склеивания трёх условий через «и» - иначе при падении будет видно только, что всё вместе неверно.
// Для ожидаемых ошибок есть отдельная конструкция: блок, внутри которого исключение обязано случиться, с проверкой типа и текста. Я такую написал: она требует именно ValueError и совпадения куска сообщения. Тест, который просто ловит любое исключение, слабый - он пройдёт и при опечатке в коде, которая даёт совсем другую ошибку.
- разбор проверки
- pytest переписывает assert так, чтобы показать промежуточные значения
- ожидаемое исключение
- блок, где ошибка обязана случиться; проверяется тип и текст
Параметризация вместо копирования
Четыре похожих случая можно написать четырьмя функциями, а можно одной с набором данных. Второе лучше по трём причинам: код короче, случаи видны списком, и при падении в отчёте указан КОНКРЕТНЫЙ набор, а не «упал тест номер три».
В моём прогоне параметризованный тест дал четыре отдельных результата из одной функции: границы 0 и 100 процентов, обычный случай и дробная цена. Именно так удобно закрывать границы - они собираются в одном месте и легко дополняются.
// Полезная привычка: складывать в набор и удачные случаи, и те, что должны падать, помечая ожидаемое поведение прямо в данных. И не увлекаться: если для одного случая нужна отдельная подготовка на десять строк, ему лучше жить отдельным тестом, а не превращать набор в кашу из условий.
- параметризация
- один тест с набором данных; каждый набор - отдельный результат
Запуск и отбор
Запускать все тесты каждый раз необязательно. Отбор по имени, по метке, по файлу, только упавшие в прошлый раз, остановка на первом падении - всё это встроено и экономит минуты на каждой итерации.
Метки - это способ разделить набор на группы: быстрые и медленные, требующие базы, требующие сети. Тогда в конвейере на каждый коммит гоняются быстрые, а полный набор - перед вливанием в основную ветку. Без такого деления набор рано или поздно становится медленным настолько, что его перестают запускать локально.
// И про код возврата: тесты сообщают результат именно им, поэтому конвейер понимает падение автоматически. Ноль - всё прошло, единица - есть упавшие, пятёрка - не найдено ни одного теста. Последнее коварно: неверный путь в настройке даёт «успех» в глазах невнимательного конвейера, если он смотрит только на отсутствие ошибок в логе.
- метки
- группы тестов: быстрые, требующие базы, требующие сети
- код возврата
- 0 прошло, 1 есть упавшие, 5 не найдено ни одного теста
Как отвечать: «Чем pytest отличается от unittest и почему обычно берут его?»
Главное отличие - в том, что тест это обычная функция с обычным оператором сравнения, а не класс с наследованием и специальными методами. При этом отчёт получается подробнее: pytest переписывает выражение проверки и показывает промежуточные значения. Я специально писал неверную проверку - в отчёте было и полученное число, и вызов, который его дал, хотя в коде стоял один голый assert. Дальше идут вещи, ради которых его и берут на практике: параметризация, когда один тест закрывает четыре случая и каждый виден в отчёте отдельно; фикстуры с областями жизни вместо методов подготовки; богатый отбор тестов по имени и меткам. И совместимость: pytest умеет запускать и тесты, написанные на unittest, поэтому переезжают обычно постепенно, а не переписыванием всего разом.
Ответ называет механику отличия, подкрепляет живым отчётом и заканчивается совместимостью. Последнее важно: на реальном проекте вопрос стоит как «как переехать», а не «что лучше».
На чём валятся
- −Склеивают три условия в одну проверку и не понимают, что именно упало.
- −Ловят любое исключение вместо конкретного типа и текста.
- −Копируют тест четыре раза вместо параметризации.
- −Не делят тесты на быстрые и медленные, и набор перестают запускать локально.
- −Не смотрят на код возврата: «не найдено ни одного теста» выглядит как успех.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Зачем
@pytest.mark.parametrize?A)Сгенерировать случайные входные данные для теста на каждый прогонB)Пометить тест как медленный, чтобы пропускать его в CI при спешкеC)Прогнать один тест на наборе входов как отдельные кейсыD)Запустить тест параллельно в нескольких процессах одновременнопоказать ответ и разбор
+C)Прогнать один тест на наборе входов как отдельные кейсы// разбор: parametrize прогоняет одну тест-функцию на списке наборов аргументов, разворачивая каждый в отдельный кейс с собственным статусом в отчёте. Это убирает копипасту похожих тестов и делает видимым, какой именно вход упал. Наборы можно комбинировать, складывая несколько parametrize.
- Что делают флаги
-xи-kу pytest?A)-x пропускает падающие тесты, -k помечает их как ожидаемо красныеB)Оба показывают покрытие кода: -x по строкам, -k по веткамC)-x запускает в несколько потоков, -k задаёт число повторов тестаD)-x останавливает на первом падении, -k фильтрует по именипоказать ответ и разбор
+D)-x останавливает на первом падении, -k фильтрует по имени// разбор: -x (или --exitfirst) прекращает прогон на первом же падении — удобно в отладке, чтобы не ждать весь набор. -k "expr" отбирает тесты по подстроке имени (-k "login and not slow"). Вместе с -q (тихий вывод) это рабочий набор для быстрых итераций локально.
- Как pytest находит тесты в проекте по умолчанию?A)По наследованию каждого теста от обязательного базового класса тест-фреймворкаB)По соглашению имён: файлы test_*.py и функции/методы test_*C)По явному списку всех тестовых файлов, который заранее прописывают в конфиге проектаD)По специальному декоратору, которым нужно пометить каждую тестовую функцию отдельно
показать ответ и разбор
+B)По соглашению имён: файлы test_*.py и функции/методы test_*// разбор: pytest сам обнаруживает тесты по соглашениям: ищет файлы test_*.py (или *_test.py), внутри — функции test_* и методы test_* в классах Test*. Ни базовый класс, ни декоратор, ни ручной реестр не нужны — достаточно следовать именованию. Правила поиска настраиваются, но дефолт покрывает большинство проектов.
- Чем удобен обычный assert в pytest по сравнению с методами вроде assertEqual?A)pytest переписывает assert и сам показывает значения обеих сторон при провалеB)assert автоматически повторяет упавший тест несколько раз для проверки стабильностиC)Ничем: assert в pytest — это ровно то же самое, что assertEqual в unittest, просто корочеD)assert работает быстрее, потому что не вычисляет выражение справа от сравнения
показать ответ и разбор
+A)pytest переписывает assert и сам показывает значения обеих сторон при провале// разбор: pytest применяет assert rewriting: при провале обычного assert a == b он показывает фактические значения обеих сторон, разницу коллекций, подробности — без ручных сообщений и без семейства assertEqual/assertTrue из unittest. Это делает тесты лаконичнее (просто assert-выражение) и понятнее при падении. Одна из причин популярности pytest.
- Нужно прогнать один тест на десяти наборах входных данных. Идиома pytest?A)Написать цикл for внутри одного теста и перебирать наборы данных вручную по очередиB)Передать наборы данных в conftest, откуда pytest сам подставит их в тестC)@pytest.mark.parametrize со списком случаев — каждый станет отдельным тестомD)Скопировать тело теста десять раз, меняя в каждой копии только входные значения
показать ответ и разбор
+C)@pytest.mark.parametrize со списком случаев — каждый станет отдельным тестом// разбор: @pytest.mark.parametrize('val,expected', [...]) прогоняет тест на каждом наборе как отдельный кейс: провал одного не скрывает остальные, в отчёте видно, какой именно случай упал. Это чище цикла (тот падает на первом же и прячет прочие) и дублирования тела. Параметры можно и складывать, получая декартово произведение.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.