Написание теста — ключевой этап разработки на Python и самый эффективный способ убедиться, что код работает должным образом, а последние внесённые изменения не нарушили функциональность вашего приложения. Для поиска ошибок в Python-программах специалисты используют среду тестирования Pytest. Она позволяет разработчику легко писать масштабируемые тестовые примеры для пользовательского интерфейса, баз данных и API.
В статье подробно рассмотрим основные моменты при работе с библиотекой и разберёмся с плюсами и минусами её применения.
Для чего нужен Pytest
Pytest — фреймворк проверки Python-кода, позволяющий запускать настраиваемые тесты для поиска ошибок на участках кода разной сложности.
По результатам опроса разработчиков, проведённого JetBrains, библиотекой пользуется каждый второй Python-разработчик.
Плюсы и минусы использования фреймворка
С ним вы быстрее сможете выполнять стандартные задачи, избегая написания громоздких кодовых конструкций, при помощи встроенных команд, экономящих время. Кроме того, используя его, можно запускать ранее созданные тесты без плагина.
Плюсы
Меньше повторяющегося кода
Чтобы создать тест в unittest, вам надо было сделать следующее:
-
Импорт соответствующего класса из стандартной библиотеки.
-
Сделать его подкласс.
-
Создать этот метод в каждом тесте.
-
Применить для них специальный метод self.assert, обрабатывающий утверждения.
Это минимальное количество задач, необходимое при тестировании в unittest, он требует многократного написания одних и тех же кодовых конструкций и поэтому не эффективен.
Библиотека Pytest упрощает процесс и даёт возможность разработчику использовать обычные функции вместе со встроенным ключевым словом — assert. Вот как будет выглядеть тот же тест, если он написан при помощи Pytest:
# test_with_Pytest.py
def test_always_passes():
assert True
def test_always_fails():
assert False
Это несложно, нет необходимости импортировать какие-либо модули или использовать дополнительные классы, код становится более детальным и легко читаемым.
Подробный отчёт об ошибках
Если тест работает некорректно, Pytest сам выдаст отчёт и в первом разделе покажет:
-
Статус системы со сведениями о версии Python, самого Pytest и других подключённых плагинов.
-
Каталог, внутри которого можно найти необходимые тесты и их конфигурацию.
-
Количество созданных на момент сканирования тестов.
В следующем разделе отображается статус каждого теста напротив его названия (на сколько он пройден в процентном отношении). Если рядом с названием стоит символ «.» — тест пройден, если F — завершился неудачно, а если E — тест закончился вызовом неожиданного исключения.
Подробная разбивка всегда сопровождает неудачные тесты и делает отладку более управляемой. В заключительных разделах отчёта указано общее состояние проводимого тестирования.
Простота в освоении
Pytest достаточно просто использовать. Вот примеры, наглядно демонстрирующие несколько способов тестирования:
# test_examples_of_assertion.py
def test_uppercase():
assert "example text".upper() == "EXAMPLE TEST"
def test_reverseList():
assert list(reverseList([5, 6, 7, 8])) == [8, 7, 6, 5]
def test_some_primes():
assert 37 in {
num
for num in range(2, 50)
if not any(num % div == 0 for div in range(2, num))
}
Тесты из предыдущего примера — короткие, автономные и выглядят как обычные функции Python, что упрощает изучение рассматриваемой библиотеки и избавляет от необходимости привлекать новые конструкции.
Применение функций-фикстур
Фикстуры — функции, позволяющие создавать наборы данных, тестировать дубликаты и работать с состояниями систем различных тестов.
Метки
Метки применяются для быстрой настройки тестов (настраиваются условия пуска и параметры для входа).
Большая экосистема плагинов
Настраиваемость Pytest делает его идеальным фреймворком для всех, кто хочет протестировать код. Добавлять новые функции легко, практически каждая часть программы может быть изменена. Неудивительно, что для Pytest доступна огромная экосистема полезных плагинов. На сегодняшний день их написано больше полутора тысячи.
Минусы
Однако у фреймворка есть свои недостатки:
-
Некоторые процессы скрыты от пользователя. Успешное освоение библиотеки требует детального изучения документации.
-
Pytest не входит в список пакетов стандартной библиотеки Python, поэтому его необходимо устанавливать дополнительно.
-
При использовании Python до 3.7 необходимо подключение одной из предыдущих версий фреймворка, соответствующих версии языка. Реестр доступных версий смотрите здесь.
-
Не все фреймворки совместимы с рассматриваемой библиотекой.
Установка
-
Pytest — среда тестирования для Python, поэтому полезно иметь некоторый опыт программирования именно на нём.
-
Pytest опирается на определённые концепции программирования, функции, переменные и циклы. Если вы с ними знакомы, вам будет легче научиться применять Pytest.
-
Чтобы использовать Pytest, на вашем компьютере должны быть установлены Python и pip (менеджер пакетов Python).
Библиотеку можно взять из каталога программного обеспечения PyPI, установив его, используя систему, управляющую Python-пакетами pip. Если вы работаете в Windows, запустите в Windows PowerShell следующее:
Если вы используете macOS или Linux, запустите это на терминале:
Написание тестов
На первом этапе необходимо подготовить тестируемый код. Рассмотрим процесс на простом примере. Нужно создать файл main.py и определить в нём функцию sum2 с двумя аргументами x и y, возвращающую результат их сложения.
def sum2(x, y):
return x + y
Следующим шагом будет проверка корректности работы созданной функции с помощью создания файла tests.py, куда мы импортируем эту функцию и добавляем остальную логику теста (test_sum2).
from main import sum2
def test_sum2():
assert sum2(15, 8) == 23
Запустить тест можно через консоль либо воспользоваться интерфейсом среды разработки (IDE) (допустим, PyCharm).
В результате выполнения тестов можно получить следующий вывод:
tests.py::test_sum2 PASSED [100%]
Далее мы можем откорректировать условия тестирования, чтобы проверить функцию на ожидание результата 0 вместо 23.
def test_sum2():
assert sum2(15, 8) == 0
Запустив тест, мы увидим, что он не может завершиться:
FAILED [100%]
tests.py:3 (test_sum2)
23 != 0
Expected :0
Actual :23
<Click to see difference>
def test_sum2():
> assert sum2(15, 8) == 0
E assert 23 == 0
E + where 23 = sum2(15, 8)
tests.py:5: AssertionError
Как именовать тесты?
Существуют ограничения по неймингу тестов, созданные для того, чтобы фреймворк мог распознать тестовые функции. Файлы и функции тестов должны именоваться по следующим правилам:
-
В начале имени файла должно стоять ключевое слово test, а в конце — test.py.
-
Для написания имени функции используется нижний регистр, а сама функция начинается с test_.
Использование ключевого слова assert
Для проверки условий теста используется ключевое слово assert. При истинном условии (True) тестирование считается пройденным, при ложном (False) — нет.
Например:
def test_true():
assert True
def test_false():
assert False
Результат:
tests.py::test_true PASSED [50%]
tests.py::test_false FAILED [100%]
tests.py:9 (test_false)
def test_false():
> assert False
E assert False
tests.py:11: AssertionError
В конце условия добавьте дополнительное сообщение для отладки, которое Pytest отобразит в случае неудачного прохождения теста:
def test_message():
assert False, 'Тест всегда провален'
Выведет:
tests.py::test_message FAILED [100%]
tests.py:0 (test_message)
def test_message():
> assert False, 'Тест всегда провален'
E AssertionError: Тест всегда провален
E assert False
tests.py:2: AssertionError
Отсутствие в тесте слова assert означает, что тест пройден:
def test_pass():
pass # оператор-заглушка, не делает ничего
Вывод:
tests.py::test_pass PASSED [100%]
Запускаем тест
Вводя команду python -m pytest в терминале, мы инициируем выполнение всех тестов, расположенных в текущей директории. Для более конкретной настройки запуска после команды укажите путь к файлу либо какую-то определённую функцию.
Например, для запуска теста из файла test.py воспользуйтесь следующей командой: pytest test.py.
А если требуется выполнить только определённую функцию, например test_sum2, пропишите:
"pytest test.py::test_sum2".
Чтобы достичь большей гибкости в настройке запуска, задайте флаги. Их перечень приведён в документации по Pytest.
Многие интегрированные среды разработки IDE (PyCharm) предоставляют графический интерфейс для работы с тестами.
Применение функций-фикстур
Фикстуры Pytest позволяют предоставлять необходимую информацию для теста, могут его дублировать или описывать его настройку. Функции фиксации способны давать на выходе большой диапазон различных значений. Каждый тест, зависящий от фикстуры, нужно явно передать функции как аргумент.
Лучшим способом понять, когда использовать фикстуры, будет моделирование рабочего процесса разработки, основанного на тестировании.
Допустим, нам необходимо создать функцию, обрабатывающую информацию, полученную от одной из конечных точек API. В ней содержится список людей, где каждая запись имеет фамилию, имя и профессию человека.
Эта функция должна вывести список строк с полными именами, за которыми следует двоеточие и их заголовок:
# format_data.py
def reformat_data(people):
... # Instructions to implement
Поскольку мы моделируем рабочий процесс разработки, первым делом нужно написать для него тест. Один из способов сделать это:
# test_format_data.py
def test_reformat_data ():
people = [
{
"given_name": "Mia",
"family_name": "Alice",
"title": "Software Developer",
},
{
"given_name": "Arun",
"family_name": "Niketa",
"title": "HR Head",
},
]
assert reformat_data(people) == [
"Mia Alice: Software Developer",
"Arun Niketa: HR Head",
]
Делаем шаг вперёд и напишем другую функцию для обработки информации и вывода её в формате значений для использования в электронных таблицах:
# format_data.py
def reformat_data(people):
... # Instructions to implement
def format_data_for_excel(people):
... # Instructions to implement
Список дел растёт, а благодаря разработке через тестирование вы можете легко планировать задачи наперёд. Новая функция будет похожа на reformat_data():
# test_format_data.py
def test_reformat_data():
# ...
def test_format_data_for_excel():
people = [
{
"given_name": "Mia",
"family_name": "Alice",
"title": "Software Developer",
},
{
"given_name": "Arun",
"family_name": "Niketa",
"title": "HR Head",
},
]
assert format_data_for_excel(people) == """given,family,title
Mia,Alice, Software Developer
Arun,Niketa,HR Head
"""
Оба теста должны снова определить переменную people, а сборка этих строк кода требует времени и дополнительных усилий.
Фикстуры могут помочь при использовании одних и тех же тестовых данных в разных тестах. С их помощью можно поместить одинаковые куски кода в одну функцию и обозначить при помощи ключевого слова @Pytest.fixture, например:
# test_format_data.py
import Pytest
@Pytest.fixture
def example_people_data():
return [
{
"given_name": "Mia",
"family_name": "Alice",
"title": "Software Developer",
},
{
"given_name": "Arun",
"family_name": "Niketa",
"title": "HR Head",
},
]
# More code
Использовать фикстуру несложно — нужно добавить ссылку на функцию в качестве аргумента. Возвращаемое значение функции-фикстуры может использоваться так:
# test_format_data.py
# ...
def test_reformat_data(example_people_data):
assert reformat_data(example_people_data) == [
"Mia Alice: Software Developer",
"Arun Niketa: HR Head",
]
def test_format_data_for_excel(example_people_data):
assert format_data_for_excel(example_people_data) == """given,family,title
Mia,Alice, Software Developer
Arun,Niketa,HR Head
"""
Теперь тесты намного меньше, но имеют чёткий обратный путь к тестовым данным.
Использование меток
Pytest даёт возможность кастомизировать процесс запуска тестов с помощью меток, которые могут быть применены как к функциям, так и к классам. Для добавления метки напишите следующую конструкцию: @pytest.mark.name_of_mark. Хотите выбрать только помеченные тесты, пропишите в терминале pytest -m name_of_mark. При желании исключить определённые метки наберите pytest -m 'name_of_mark'. Каждый тест может иметь несколько меток. Для получения списка доступных меток вбейте в терминал pytest --markers или обратитесь к документации для более подробной информации о них.
Пропуск теста
Для пропуска теста вам следует добавить пометку skip. В качестве аргумента можно передавать параметр reason='по какой причине пропущен'. Например:
@pytest.mark.skip(reason='Тестовый пропуск')
def test_skipped():
pass
Результат:
SKIPPED (Тестовый пропуск) [100%]
Skipped: Тестовый пропуск
Пропуск теста при условии
Метка skipif принимает пару аргументов. Первый из них представляет собой условие. При его соблюдении (результатом будет True), тест будет опущен, иначе (результатом будет False) тест будет выполнен:
x = 1
@pytest.mark.skipif(x > 0, reason='Тестовый пропуск')
def test_skipped_if():
pass
Результат такой же, как и в предыдущем примере.
Ожидаемый провал теста
Применение метки xfail к тесту может привести к двум различным исходам. Если выполнение теста завершится успешно, Pytest отметит его как xpass; при ожидаемом неудачном выполнении тест помечается xfail. Такой вывод не повлияет на общий результат выполнения тестового набора:
@pytest.mark.xfail(reason='Намеренный провал')
def test_xfailed():
assert False
Выведет:
XFAIL (Намеренный провал) [100%]
@pytest.mark.xfail(reason='Намеренный провал')
def test_xfailed():
> assert False
E assert False
tests.py:49: AssertionError
У xfail, как и у skipif, можно задать условие (и ждать неудачи только в случае его выполнения). Кроме того, xfail предоставляет возможность:
-
добавлять исключения при помощи ключевого слова raises;
-
полностью исключить выполнение теста при помощи конструкции run=False (при этом тест помечается как xfail);
-
создать условие, при котором неудача при прохождении теста приведёт к провалу набора тестов при помощи конструкции strict=True.
Подробнее о работе с xfail смотрите в документации.
Пользовательские метки
Дополнительно к автоматическим меткам есть возможность использовать индивидуальные. Это удобно, если требуется разделить тесты на несколько групп и проводить их запуск отдельно. Чтобы сформировать свою метку, просто укажите её название:
import pytest
@pytest.mark.my_mark
def test_1():
pass
def test_2():
pass
@pytest.mark.my_mark
def test_3():
pass
@pytest.mark.my_mark
def test_4():
pass
def test_5():
pass
Для запуска теста, помеченного my_mark при вводе в терминал команды pytest --no-summary -m my_mark tests.py, получите на выходе следующее:
collected 5 items / 2 deselected / 3 selected
tests.py .. [100%]
===== 3 passed, 2 deselected, 3 warnings in 0.02s =====
Только три файла из пяти запустились с необходимой меткой, а также были зафиксированы три предупреждения. Появилась ошибка от Pytest, указывающая на незарегистрированную метку my_mark в pytest.ini. Важно убедиться, что вы использовали собственную метку, а не допустили ошибку при редактировании встроенной. Инструкции по регистрации собственной метки можно найти в документации.
Заключение
Использование готовых решений для оптимизации тестирования — неплохая практика по поиску проблем в новых или обновлённых приложениях. Pytest — основной инструмент тестирования в среде Python-разработчиков. Он помогает программисту оптимизировать время и усилия для разработки и включения теста в работу, его гибкая система плагинов позволяет расширять функциональные возможности. Его можно применять при тестировании баз данных, пользовательских интерфейсов и различных API.