Руководство по Pytest: как тестировать код в Python
Гайд по самому популярному среди Python-разработчиков фреймворку для тестирования.
Иллюстрация: Катя Павловская для Skillbox Media
Написать рабочий код — полдела: нужно добиться, чтобы он выдавал корректные результаты.
Можно тестировать программу вручную: запускать её вновь и вновь в разных условиях и проверять, правильно ли всё работает. Но лучше, конечно, автоматизировать процесс и написать код, который будет проверять другой код. Чтобы упростить эту задачу, и придумали Pytest.
Из этой статьи вы узнаете:
- Что такое Pytest
- Какие у него преимущества и недостатки
- Как установить Pytest
- Как писать тесты
- Что такое фикстуры в Pytest
- Какие в Pytest есть тестовые метки
Что такое Pytest
Pytest — это фреймворк для тестирования кода на Python. Он был разработан в 2004 году, но до сих пор регулярно обновляется и позволяет не только писать тесты, но и создавать для них окружение, а также настраивать параметры запуска.
Согласно исследованию JetBrains, Pytest использует каждый второй питонист.
Преимущества и недостатки Pytest
Успешность Pytest в сравнении с конкурентами (например, Unittest) легко объясняется его преимуществами:
- Лаконичный код. В синтаксисе Pytest нет громоздких конструкций, как в том же Unittest. Простой тест может состоять всего из двух строк.
- Подробные отчёты об ошибках. Если тест работает неправильно, Pytest сам объяснит, в чём дело.
- Универсальный оператор assert. Не нужно запоминать разные его виды, как в Unittest.
- Фикстуры. Позволяют создавать контекст сразу для группы тестов.
- Метки. Можно настраивать поведение тестов: задавать условия запуска, передавать одному и тому же тесту разные входные данные и так далее.
- Умеет запускать тесты других фреймворков. С Pytest совместимы Unittest, Doctest и Nose.
- Множество плагинов. Если какой-то функции «из коробки» не хватает, для Pytest написано больше тысячи плагинов. Причём 180 из них обновлялись в 2023 году, ещё 360 — в 2022-м.
Тем не менее недостатки у фреймворка тоже есть:
- Неявность и магия. Обратная сторона простоты и лаконичности есть: многие процессы происходят «под капотом». Чтобы разобраться в них детально, придётся штудировать документацию.
- Не входит в стандартную библиотеку. Pytest нужно устанавливать отдельно. Если у вас старая (ниже 3.7) версия Python, то нужно будет подключать соответствующую версию фреймворка. Найти их список можно здесь.
- Другие фреймворки несовместимы с Pytest. Неизбежное следствие лидерского, практически королевского, статуса: Pytest может запускать тесты других фреймворков, но ни один из других фреймворков не может запускать тесты Pytest. Вот такая современная трактовка древнеримской пословицы «Что позволено Юпитеру, не позволено быку».
Как установить Pytest
Pytest входит в большинство пакетов Python. Его последняя версия доступна для Python 3.7+ и PyPy 3. Чтобы установить его в свою виртуальную среду, используйте команду:
pip install -U pytest
Другой способ — пакетный менеджер вашей IDE. Найдите в нём модуль с названием pytest и загрузите его.
Как писать тесты
Для начала нужен код, который мы будем тестировать. Создадим файл main.py и функцию sum2. Она будет принимать на вход два аргумента и возвращать их сумму:
Теперь проверим, корректно ли она работает. Для этого создадим файл tests.py, импортируем в него sum2 и напишем test_sum2:
Чтобы запустить тесты, введём в консоль команду pytest. Альтернативный вариант — использовать интерфейс вашей IDE. Например, PyCharm позволяет запустить файл целиком или тестовую функцию в отдельности.
Получаем вот такой результат:
Теперь изменим наш тест: пусть он ожидает получить не 23, а 0:
Получим сообщение о том, что тест не пройден:
Ограничения нейминга
Чтобы Pytest воспринимал функции тестовыми, файлы и сами тесты должны быть названы определённым образом:
- название файла должно начинаться на test или заканчиваться на test.py;
- название функции должно быть написано в нижнем регистре и начинаться с test_.
Как работает assert
Ключевому слову assert можно передать любое условие. Если оно правдиво (результат True) — тест пройден, если ложно (результат False) — не пройден.
Таким образом можно писать минимальные тесты:
Результат:
Через запятую после условия можно написать отладочное сообщение. Pytest выведет его, если тест провалится:
Результат:
Если в тесте нет assert, он считается пройденным:
Результат:
Примечание
В одном тесте может быть сразу несколько операторов assert, но делать так мы не рекомендуем. Лучше руководствоваться правилом «Один тест — одна сущность, одна функция — один assert».
Запуск тестов
Команда терминала pytest запускает все тесты текущего каталога. Чтобы управлять условиями запуска, укажите после неё путь до файла или даже отдельной функции.
- Команда для запуска файла tests.py: pytest tests.py.
- Команда для запуска функции test_sum2 и только её: pytest tests.py: test_sum2.
Для более гибкого запуска можно дополнительно добавлять флаги. Их список есть в документации Pytest.
Помимо команд терминала можно использовать графический интерфейс вашей IDE. Описанные в этой статье тесты мы запускаем через инструменты PyCharm.
Фикстуры в Pytest
Фикстуры — это функции, которые создают окружение вокруг тестов. Они удобны, когда нужно передать одни и те же входные данные нескольким тестам.
Допустим, у нас есть несколько функций в main.py:
Напишем для каждой из них по тесту в файле tests.py. В качестве тестового массива возьмём список простых чисел от 1 до 50. Создавать его будем с помощью цикла for-else:
Пока во всех тестовых функциях мы используем одну и ту же громоздкую конструкцию, создающую список простых чисел. Вынесем её в отдельную фикстуру. Для этого явно импортируем модуль pytest:
Чтобы объявить функцию фикстурой, используем перед ней декоратор @pytest.fixture():
Теперь передадим эту фикстуру во все тесты, где она нужна. Обращаясь к фикстуре, у неё не нужно писать круглые скобки: как будто это не функция, а переменная.
Сами тесты в итоге получаются такие:
Финализатор
Если хотите, чтобы после запуска теста выполнялся ещё какой-то скрипт, это также можно сделать через фикстуры. Для этого вместо ключевого слова return используйте yield. Код, написанный после yield, и будет выполняться по завершении теста.
Изменим в нашем примере с простыми числами фикстуру get_prime_nums() и добавим в неё финализатор:
При запуске тестов получаем такой результат:
Если тест не был пройден (то есть assert получил False), код из финализатора всё равно выполняется.
Области действия фикстур
У фикстур можно настраивать область действия, в которой они существуют. По умолчанию она равна функции. Это значит, что, когда тестовая функция прекращает свою работу, фикстура финализируется и уничтожается. При следующем вызове фикстура создаётся заново. Это хорошо видно по прошлому примеру с финализатором.
Область действия фикстуры указывается в её декораторе аргументом scope='область действия'. Всего есть пять уровней:
- 'function' — для функции;
- 'class' — для класса;
- 'module' — для модуля (то есть py-файла);
- 'package' — для пакета;
- 'session' — для всей сессии тестирования.
Изменим у фикстуры get_prime_nums область действия на module.
Посмотрим, как изменится работа тестов:
В отличие от прошлого примера, фикстура здесь вызывается только один раз — в первой функции, которая её использует. Затем результат работы кэшируется. Финализатор тоже срабатывает только единожды, когда заканчивается файл.
Иерархии фикстур
Одному тесту можно передать сколько угодно фикстур, указывая их через запятую. Их можно передавать и другим фикстурам — тоже в любом количестве.
Например, фикстуру get_prime_nums можно разбить на несколько (хотя в нашем случае в этом нет практического смысла):
Вы всегда явно указываете, какие фикстуры используете в функции или другой фикстуре. Это позволяет удобно отслеживать зависимости данных и управлять ими.
Примечание
Фикстуры с более широкой областью действия нельзя встраивать с фикстуры меньшего уровня.
Автоиспользование фикстур
Иногда может быть полезным, чтобы фикстура запускалась всегда, даже если функция её не вызывает. Например, когда перед выполнением тестов нужно залогиниться в системе.
В таких случаях можно указать параметр autouse в декораторе: @pytest.fixture(autouse=True).
Будьте осторожны при использовании autouse. Такие фикстуры могут создавать неявные зависимости и менять данные непредсказуемым для вас путём. Особенно если их много и они находятся в сложной иерархии.
Метки тестов
Pytest позволяет настраивать запуск тестов, применяя к ним метки. Использовать их можно не только с тестовыми функциями, но и с целыми классами. Чтобы добавить метку, нужно написать декоратор: @pytest.mark.*название метки*.
В Pytest можно сделать так, чтобы запускались только помеченные тесты. Для этого используют команду терминала с аргументом -m: pytest -m *название метки*. Можно и наоборот: запустить все тесты, кроме помеченных. В таком случае команда выглядит так: pytest -m 'not название метки'.
У одного теста или класса может быть сколько угодно меток. Посмотреть их список можно командой pytest --markers и в документации. Мы расскажем об основных.
Пропуск теста
Чтобы пропустить тест, поставьте метку skip. В качестве аргумента ей можно передать необязательный параметр reason='причина пропуска'. Например:
Результат:
Пропуск теста при условии
Метка skipif получает два аргумента. Первый — это условие. Если оно выполняется (результат True) — тест пропускается, если нет (результат False) — тест выполняется как обычно. Во втором аргументе, как и в случае со skip, можно передать строку с причиной пропуска:
Результат тот же самый, что и в прошлом случае:
Ожидаемый провал теста
Тест под меткой xfail может выдать два результата. Если тест будет пройден, Pytest пометит его XPASS, если ожидаемо провален — XFAIL. Ни один из вариантов не вызовет провала общего набора тестов:
Результат:
У xfail есть несколько аргументов. Как в skipif, вы можете указать условие (и ожидать провал только при нём) и передать параметр reason. Дополнительно к этому xfail позволяет:
- добавить исключение в raises=*название исключения*;
- вообще не выполнять тест в run=False (тогда он автоматически будет засчитан как XFAIL);
- сделать, чтобы провал теста вызывал провал всего тестового набора в strict=True.
Подробнее о возможностях xfail — в документации.
Параметризация
Метка parametrize позволяет вызывать один и тот же тест с разными входными данными. Это полезно, когда мы хотим проверить несколько случаев.
Например, у нас есть функция, которая пишет, положительное число или отрицательное:
Сначала проверим, правильно ли она обрабатывает положительные числа: целые, дробные и очень маленькие. Без параметризации нам пришлось бы писать сразу три однотипных теста:
Результат:
А потом нужно будет писать такие же три теста для отрицательных чисел и ещё один для нуля. Итого семь тестов для одной маленькой функции. Нерационально. Тут-то на помощь и приходит параметризация.
Метка parametrize получает два аргумента: название переменной и список её значений. Название переменной передаём тесту (точно так же, как фикстуру). Получается вот так:
Результат:
Можно в одной строке через запятую передать сразу несколько переменных. Тогда каждый элемент списка задаётся кортежем, в котором мы по очереди перечисляем значения этих переменных:
Результат:
Если одному тесту передать сразу несколько меток parametrize, он запустит все их возможные комбинации. Пример для наглядности:
Результат:
Пользовательские метки
Помимо встроенных меток вы можете использовать свои собственные. Это удобно, когда нужно разбить все тесты по нескольким группам и запускать отдельно друг от друга.
Чтобы создать собственную метку, просто используйте её имя:
Используем команду pytest --no-summary -m my_mark tests.py, чтобы запустить только тесты с меткой my_mark. Получим такой результат:
Как видим, из пяти файлов было запущено только три — на которых стояла нужная метка.
Также мы получили три предупреждения. Это Pytest обращает наше внимание на то, что метка my_mark не зарегистрирована в файле pytest.ini. Он уточняет: вы точно воспользовались собственной меткой, а не опечатались при написании встроенной?
О том, как зарегистрировать собственную метку, тоже можно прочитать в документации.
Резюмируем
Pytest — самый популярный среди питонистов фреймворк для тестирования. Он позволяет писать меньше однотипного кода, чем встроенный Unittest, и может работать без тестовых классов. Вот его базовые инструменты:
- Ключевое слово assert отвечает за результат тестирования. Если заданное после него условие правдиво — тест пройден, если оно ложно — провален.
- Фикстуры — дополнительные функции, в которых можно задавать окружение тестов. Они могут использовать другие фикстуры, создавая целые иерархии.
- Метки — декораторы, которые позволяют корректировать поведение тестов: пропускать их, ожидать определённых результатов, передавать разные входные данные и так далее. Можно создавать свои пользовательские метки.
Больше интересного про код — в нашем телеграм-канале. Подписывайтесь!