Что такое фреймворк Pytest
Pytest — это один из самых популярных фреймворков для тестирования кода на языке программирования Python. Он прост и гибок в использовании, имеет широкие возможности для настройки и позволяет писать компактные тесты. Он был выпущен в 2004 году в рамках проекта PyPy.
Преимущества Pytest
- Простота использования и изучения благодаря интуитивно понятному синтаксису, колоссальному количеству документации и примеров, активному и обширному сообществу разработчиков, так как для языка Python Pytest — это очень широко используемый инструмент.
- Большое количество разнообразных плагинов, которые позволяют добавить новые возможности и функции, а также поддержка пользовательских плагинов — каждый тестировщик может создать плагин для потребностей конкретного проекта.
- Возможность интеграции с другими инструментами и фреймворками.
- Поддержка параметризации, фикстур и меток, благодаря чему можно сокращать количество кода, лучше подготавливать окружение, группировать тестовые функции и эффективнее управлять их выполнением.
- Предоставление отчетов об ошибках, что позволяет быстро разбираться в возникающих проблемах.
Недостатки
- Несмотря на простой синтаксис, для реализации сложных сценариев может потребоваться детальное изучение документации и просмотр множества примеров.
- Иногда тестировщики допускают следующую ошибку: они избыточно используют возможности параметризации и меток, что приводит к усложнению структуры кода и его поддержки, хотя эти возможности, напротив, созданы для облегчения тестирования.
- Не входит в стандартную библиотеку Python — необходимо устанавливать дополнительно.
Как установить Pytest
Для установки актуальной версии необходим Python 3.8+, либо PyPy3. Установка Pytest осуществляется через стандартный менеджер пакетов pip. В командной строке нужно выполнить следующую команду:
pip install -U pytest
После этого можно проверить, какая версия была установлена:
pytest --version
В результате будет выведен номер версии, после чего можно продолжать работу.
Как писать тесты
Перед тем как написать тест, нужно написать программу (функцию), которая будет протестирована. Допустим, это будет простая функция multiply — результатом ее работы является произведение двух аргументов. Она прописана в файле main.py:
def multiply(x, y):
return x * y
После этого в файле tests.py можно написать функцию test_multiply, которая и будет проверять правильность работы multiply:
# Импорт multiply
from main import multiply
def test_multiply():
assert multiply(3, 4) == 12
Здесь ожидается, что результат умножения 3 на 4 равен 12, что верно. В Pytest запуск тестов осуществляется следующей командой:
pytest
После выполнения можно увидеть следующую запись:
tests.py::test_multiply PASSED [100%]
Если же ожидаться будет неверное значение, например, такое:
from main import multiply
def test_multiply():
assert multiply(3, 4) == 11
То результатом (здесь он немного сокращен) станет следующая запись:
tests.py::test_multiply FAILED [100%]
================================== FAILURES ===================================
_________________________________ test_multiply _______________________________
def test_multiply():
> assert multiply(3, 4) == 11
E assert 12 == 11
E + where 12 = multiply(3, 4)
tests.py:5: AssertionError
=========================== short test summary info ===========================
FAILED tests.py::test_multiply - assert 12 == 11
Можно заметить, что выбрасывается ошибка AssertionError — утверждение неверно.
Обычно пишутся и дополнительные тестовые функции, которые покрывают пограничные случаи или проверяют обработку исключений. В данном случае острой необходимости в них нет, но можно дополнительно проверить правильность работы программы при передаче в качестве аргументов нуля и отрицательных значений.
Синтаксис и ограничения
Библиотека Pytest имеет некоторые особенности именования, которые необходимо соблюдать. Если пренебрегать этими правилами, то тестовые функции не будут обнаружены, запущены и выполнены. Итак, названия файлов, содержащих тесты, должны либо начинаться с test, либо заканчиваться на _test.py (в демонстрации выше это tests.py). Названия тестовых функций должны начинаться с test_ (как test_multiply()). При этом если для группировки тестов по смыслу используется класс, то его можно называть как угодно. Главное — соблюдать правила именования функций внутри него:
class TestOperations:
def test_multiplication(self):
assert 2 * 3 == 6
def test_division(self):
assert 6 / 3 == 2
Как работает assert
Ключевое слово assert используется для проверки условий. Соответственно, если условие выполняется, то тест считается пройденным, если не выполняется — не пройденным. Если вообще не добавлять это ключевое слово, то тест будет пройден.
Запуск тестов
Запуск может осуществляться как через командную строку (console), так и через интерфейс IDE. Рассмотрим основные команды.
Для запуска всех тестов в данном каталоге и его подкаталогах достаточно использовать команду pytest.
Для запуска определенного тестового модуля (файла) используется команда pytest tests.py, где tests.py — это название модуля.
Также можно запустить отдельную тестовую функцию с помощью pytest tests.py::test_multiply, где test_multiply — это наименование функции, а tests.py — модуля.
Что такое фикстуры
Фикстуры — это специальные функции, которые позволяют подготовить тестовое окружение перед выполнением тестов. Другими словами, в Pytest фикстуры создают данные, объекты и другие элементы, необходимые для проведения тестирования программы. Рассмотрим применение таких функций на примере:
# Файл main.py
def to_uppercase(strings):
return [s.upper() for s in strings]
def string_lengths(strings):
return [len(s) for s in strings]
Есть две функции: первая возвращает строки, преобразованные к верхнему регистру, а вторая — длины всех строк в коллекции. Для того, чтобы осуществить проверку корректности выполнения этих функций, нужно будет либо вручную прописывать элементы коллекции, либо генерировать их с помощью цикла. Но в обоих случаях код будет дублироваться. В таком случае можно использовать фикстуру, в которой будет создаваться коллекция:
# Файл tests.py
import pytest
from main import *
# Необходимо добавить декоратор
@pytest.fixture()
def get_test_strings():
test_strings = ["hello", "world", "we", "are", "trying", "fixture"]
return test_strings
Внутри get_test_strings() создается список с текстовыми элементами. Теперь эту фикстуру можно передать в тестовые функции:
def test_to_uppercase(get_test_strings):
test_strings = get_test_strings
assert to_uppercase(test_strings) == ["HELLO", "WORLD", "WE", "ARE", "TRYING", "FIXTURE"]
def test_string_lengths(get_test_strings):
test_strings = get_test_strings
assert string_lengths(test_strings) == [5, 5, 2, 3, 6, 7]
Таким образом сокращается дублирование кода и ускоряется процесс разработки. Чем больше функций, которые необходимо протестировать, тем сильнее выручают фикстуры.
Финализатор
Финализатор — это часть фикстуры, которая выполняется уже после завершения теста. Финализаторы могут использоваться, если нужно выполнить дополнительные действия или очистить тестовые данные, так как часто при тестировании задействованы различные ресурсы, например, временные файлы. Для того чтобы добавить финализатор, нужно использовать ключевое слово yield вместо return — это рекомендованный в документации метод. Все, что будет написано после yield, выполнится после тестирования вне зависимости от того, было оно успешным или завершилось ошибкой:
@pytest.fixture()
def get_test_strings():
test_strings = ["hello", "world", "we", "are", "trying", "fixture"]
# yield вместо return
yield test_strings
# То, что должно выполняться после завершения тестов
print("get_test_strings() успешно использована")
Есть и другой способ введения финализаторов, для него понадобится метод addfinalizer — в документации пользоваться таким способом не рекомендуют:
@pytest.fixture()
def get_test_strings(request):
test_strings = ["hello", "world", "we", "are", "trying", "fixture"]
# Финализатор, который будет выполнен после тестов
def print_message():
print("get_test_strings() успешно использована")
# Используем метод addfinalizer
request.addfinalizer(print_message)
return test_strings
Такой код менее читаемый и более громоздкий.
Области действия фикстур
Фикстуры могут иметь различные области действия — этот параметр определяет, когда они будут создаваться и уничтожаться. Области действия нужны для контроля жизненного цикла фикстур и упрощения управления ими. Существует 5 областей действия:
- function — значение по умолчанию, фикстура создается и уничтожается для функции;
- class — создание и уничтожение для класса;
- module — создание и уничтожение для модуля (файла);
- package — создание и уничтожение для пакета;
- session — фикстура создается и уничтожается один раз за всю сессию тестирования.
Этот параметр задается как аргумент декоратора — scope = “нужная область”:
@pytest.fixture(scope="module")
def get_test_strings():
test_strings = ["hello", "world", "we", "are", "trying", "fixture"]
return test_strings
В таком случае фикстура будет создана только один раз перед запуском и для всех тестов файла tests.py, а после их выполнения уничтожится.
Иерархии фикстур
Фреймворк Pytest поддерживает создание иерархии фикстур: передавать их можно в неограниченном количестве, при этом как тестовым функциям, так и другим фикстурам. Это позволяет создавать сложные зависимости, лучше подготовить данные перед началом тестирования и сделать код более компактным. Иерархию можно создать так:
@pytest.fixture()
def get_test_strings():
test_strings = ["hello", "world"]
return test_strings
# Дополнительная фикстура
@pytest.fixture()
def get_prefix():
return "TEST:"
# Передаем в test_to_uppercase две фикстуры, указанные через запятую
def test_to_uppercase(get_test_strings, get_prefix):
test_strings = get_test_strings
prefixed_strings = [get_prefix + s for s in test_strings]
assert to_uppercase(prefixed_strings) == ["TEST:HELLO", "TEST:WORLD"]
Автоиспользование фикстур
Бывают ситуации, когда необходимо, чтобы фикстура применялась даже тогда, когда она явно не вызывается в тестовых функциях. Чтобы создать такое поведение, необходимо задать параметр autouse со значением True:
@pytest.fixture(autouse=True)
def start_and_end():
print("\nНачало")
yield
print("\nКонец")
def test_example_1():
assert 1 == 1
def test_example_2():
assert 2 == 2
def test_example_3():
assert 3 == 3
start_and_end будет автоматически выполняться для всех трех следующих функций и это можно отследить, так как перед тестом и после него будут добавляться соответствующие сообщения. Результат запуска кода выше будет таким:
Начало
tests.py::test_example_1 PASSED [ 33%]
Конец
Начало
tests.py::test_example_2 PASSED [ 66%]
Конец
Начало
tests.py::test_example_3 PASSED [100%]
Конец
Метки тестов
Метки — это еще один инструмент, придающий гибкости фреймворку. Они помогают правильнее организовать тестовые функции и лучше управлять ими. Для определения меток используется декоратор @pytest.mark. Для запуска всех тестов с определенной меткой используется следующая команда:
pytest -m <наименование метки>
Пропуск теста
Если возникает необходимость в пропуске теста, то можно поставить метку skip:
# reason — это опциональный параметр, его значение будет выведено, как причина пропуска
@pytest.mark.skip(reason="Пока что не нужно запускать")
def test_to_uppercase(get_test_strings):
test_strings = get_test_strings
assert to_uppercase(test_strings) == ["HELLO", "WORLD", "WE", "ARE", "TRYING", "FIXTURE"]
Также можно настроить пропуск теста только после проверки определенного условия, для этого используется метка skipif:
@pytest.mark.skipif(sys.version_info < (3, 6), reason="Нужна версия Python 3.6 или выше")
def test_to_uppercase(get_test_strings):
test_strings = get_test_strings
assert to_uppercase(test_strings) == ["HELLO", "WORLD", "WE", "ARE", "TRYING", "FIXTURE"]
Проверка результата (здесь он сокращен, сохранена только часть, которая иллюстрирует результат использования skipif):
tests.py::test_to_uppercase SKIPPED [100%]
============================== warnings summary ===============================
tests.py::test_to_uppercase
Нужна версия Python 3.6 или выше
Ожидаемый провал теста
Любой тест можно сделать ожидаемо провальным с помощью метки xfail:
@pytest.mark.xfail
def test_to_uppercase(get_test_strings):
test_strings = get_test_strings
assert to_uppercase(test_strings) == ["HELLO", "WORLD", "WE", "ARE", "TRYING", "FAILURE"]
При использовании xfail также можно задать условие и reason:
@pytest.mark.xfail(sys.version_info < (3, 6), reason="Нужна версия Python 3.6 или выше")
def test_to_uppercase(get_test_strings):
test_strings = get_test_strings
assert to_uppercase(test_strings) == ["HELLO", "WORLD", "WE", "ARE", "TRYING", "FIXTURE"]
Если тест провалится, то вывод будет таким:
tests.py::test_to_uppercase XFAIL [100%]
================================== warnings summary ===========================
tests.py::test_to_uppercase
Нужна версия Python 3.6 или выше
Параметризация
Параметризация нужна для того, чтобы запускать один и тот же текст с разными наборами входных данных — она тоже позволяет сделать код более компактным за счет сокращения дублирования кода. Параметризация задается меткой parametrize:
# Есть функция divide, проверку корректности выполнения которой нужно провести
def divide(x, y):
return x / y
# После метки parametrize здесь указаны x и y, которые являются аргументами функции и
# expected_result — ожидаемый результат. Далее в том же порядке перечисляются значения заданных
# параметров
@pytest.mark.parametrize("x, y, expected_result", [
(10, 2, 5),
(-20, 10, -2),
(-8, -2, 4),
])
def test_divide(x, y, expected_result):
assert divide(x, y) == expected_result
Проверка результата:
tests.py::test_divide[10-2-5] PASSED [ 33%]
tests.py::test_divide[-20-10--2] PASSED [ 66%]
tests.py::test_divide[-8--2-4] PASSED [100%]
Пример использования Pytest
Рассмотрим полноценный пример с применением различных возможностей Pytest, описанных в этом тексте:
# Файл main.py
# Есть три функции: для поиска площади, периметра и проверки, является ли фигура квадратом
def calculate_area(length, width):
return length * width
def calculate_perimeter(length, width):
return 2 * (length + width)
def is_square(length, width):
return length == width
# Файл tests.py
import pytest
from main import *
# Будет использоваться для ожидаемо провального test_calculate_area_xfail
@pytest.fixture()
def setup_data():
return 3, 4, 14
@pytest.mark.parametrize("length, width, expected_area", [
(3, 4, 12),
(5, 5, 25),
(2, 8, 16),
])
def test_calculate_area(length, width, expected_area):
assert calculate_area(length, width) == expected_area
@pytest.mark.xfail(reason="3*4 != 14")
def test_calculate_area_xfail(setup_data):
length, width, expected_area = setup_data
assert calculate_area(length, width) == expected_area
@pytest.mark.parametrize("length, width, expected_perimeter", [
(3, 4, 14),
(5, 5, 20),
(2, 8, 20),
])
def test_calculate_perimeter(length, width, expected_perimeter):
assert calculate_perimeter(length, width) == expected_perimeter
@pytest.mark.parametrize("length, width, expected_result", [
(3, 4, False),
(5, 5, True),
(2, 8, False),
])
def test_is_square(length, width, expected_result):
assert is_square(length, width) == expected_result
Аналоги Pytest
Несмотря на то что тест кода на Python часто выполняется через Pytest, у него есть аналоги:
- Unittest является частью стандартной библиотеки Python. Он тоже имеет широкое распространение, хорошую документацию и позволяет структурировать тесты. При этом Unittest имеет более громоздкий синтаксис и меньше встроенных возможностей;
- Robot Framework — это универсальная среда для автоматизации тестирования. Robot Framework имеет читаемый синтаксис и множество плагинов, что делает фреймворк расширяемым. При этом он менее гибкий, а его изучение может быть более сложным, чем изучение Pytest.