Тестирование
Чем pytest отличается от unittest?
Две самые популярные библиотеки — unittest и pytest.
unittest - библиотека по умолчанию встроена в стандартную библиотеку языка Python.
По формату написания тестов она сильно напоминает библиотеку JUnit, используемую в языке Java для написания тестов:
- тесты должны быть написаны в классе;
- класс должен быть отнаследован от базового класса unittest.TestCase;
- имена всех функций, являющихся тестами, должны начинаться с ключевого слова test;
- внутри функций должны быть вызовы операторов сравнения (assertX) — именно они будут проверять наши полученные значения на соответствие заявленным.
Пример использования unittest
import unittest
class SquareEqSolverTestCase(unittest.TestCase):
def test_no_root(self):
res = square_eq_solver(10, 0, 2)
self.assertEqual(len(res), 0)
def test_single_root(self):
res = square_eq_solver(10, 0, 0)
self.assertEqual(len(res), 1)
self.assertEqual(res, [0])
Аргументы “за” unittest
- Является частью стандартной библиотеки языка Python: не нужно устанавливать ничего дополнительно;
- Гибкая структура и условия запуска тестов. Для каждого теста можно назначить теги, в соответствии с которыми будем запускаться либо одна, либо другая группа тестов;
- Быстрая генерация отчетов о проведенном тестировании, как в формате plaintext, так и в формате XML.
Аргументы “против” unittest
- Для проведения тестирования придётся написать достаточно большое количество кода (по сравнению с другими библиотеками);
- Из-за того, что разработчики вдохновлялись форматом библиотеки JUnit, названия основных функций написаны в стиле camelCase (например setUp и assertEqual);
- В языке python согласно рекомендациям pep8 должен использоваться формат названий snake_case (например set_up и assert_equal).
Pytest - наиболее популярный фреймворк с открытым исходным кодом из всех, представленных здесь.
Pytest позволяет провести модульное тестирование (тестирование отдельных компонентов программы), функциональное тестирование (тестирование способности кода удовлетворять бизнес-требования), тестирование API (application programming interface) и многое другое. Формат кода
Написание тестов здесь намного проще, нежели в unittest. Вам нужно просто написать несколько функций, удовлетворяющих следующим условиям:
- Название функции должно начинаться с ключевого слова test;
- Внутри функции должно проверяться логическое выражение при помощи оператора assert.
Пример использования pytest:
def test_no_root():
res = square_eq_solver(10, 0, 2)
assert len(res) == 0
def test_single_root():
res = square_eq_solver(10, 0, 0)
assert len(res) == 1
assert res == [0]
Аргументы “за” pytest
- Позволяет писать компактные (по сравнению с unittest) наборы тестов;
- В случае возникновения ошибок выводится гораздо больше информации о них;
- Позволяет запускать тесты, написанные для других тестирующих систем;
- Имеет систему плагинов (и сотни этих самых плагинов), расширяющую возможности фреймворка. Примеры таких плагинов: pytest-cov, pytest-django, pytest-bdd;
- Позволяет запускать тесты в параллели (при помощи плагина pytest-xdist).
Аргументы “против” pytest
- pytest не входит в стандартную библиотеку языка Python. Поэтому его придётся устанавливать отдельно при помощи команды pip install pytest;
- совместимость кода с другими фреймворками отсутствует. Так что, если напишете код под pytest, запустить его при помощи встроенного unittest не получится.
Что такое параметризованные тесты (@pytest.mark.parametrize)?
Параметризованные тесты в pytest - это мощный механизм для запуска одного и того же теста с разными наборами входных данных и ожидаемых результатов.
Декоратор @pytest.mark.parametrize позволяет определить несколько наборов параметров для тестовой функции, чтобы избежать дублирования кода.
import pytest
@pytest.mark.parametrize("input1, input2, expected", [
(1, 2, 3),
(5, 3, 8),
(-1, 1, 0),
])
def test_addition(input1, input2, expected):
assert input1 + input2 == expected
Как это работает
-
Первый аргумент - строка с именами параметров через запятую
-
Второй аргумент - список кортежей с наборами значений
-
Pytest запустит тест один раз для каждого кортежа
Что такое фикстуры? Как их создать?
Фикстура — это заранее подготовленное, известное состояние среды тестирования (данные, объекты, настройки), которое необходимо для корректного выполнения тестов и гарантирует их повторяемость. Фикстуры позволяют избежать дублирования кода настройки и очистки, делая тесты чище и проще для чтения. Для их создания используется специальный синтаксис в тестовых фреймворках, например, декоратор @pytest.fixture в Python.
Фикстуры (fixtures) в pytest - это функции, которые предоставляют тестовые данные, устанавливают предварительные условия и выполняют очистку после тестов.
Фикстуры - это способ инжекции зависимостей в тесты. Они выполняются до (и после) тестовых функций и предоставляют им необходимые ресурсы.
Создание фикстуры
import pytest
@pytest.fixture
def database_connection():
# Setup - выполняется ДО теста
connection = connect_to_database()
print("Setting up database connection")
yield connection # передача контроля тесту
# Teardown - выполняется ПОСЛЕ теста
connection.close()
print("Closing database connection")
def test_database_operations(database_connection):
result = database_connection.query("SELECT * FROM users")
assert len(result) > 0
Чем mock отличается от stub?
Mock и Stub - это тестовые дублеры (test doubles), но они служат разным целям и имеют важные различия.
Stub (Заглушка) предоставляет предопределенные ответы на вызовы методов.
Характеристики Stub:
- Простая замена реального объекта
- Возвращает фиксированные данные
- Не проверяет как используется
- Пассивный - только отвечает на запросы
Пример Stub:
# Stub для сервиса погоды
class WeatherServiceStub:
def get_temperature(self, city):
predefined_data = {
"Moscow": 20,
"London": 15,
"Paris": 18
}
return predefined_data.get(city, 25)
def test_weather_app():
weather_stub = WeatherServiceStub()
temp = weather_stub.get_temperature("Moscow")
assert temp == 20
Mock - это объект, который регистрирует вызовы своих методов и может проверять их.
Характеристики Mock: - Проверяет взаимодействия - Запоминает вызовы методов - Может выбрасывать исключения - Активный - верифицирует поведение
Пример Mock:
from unittest.mock import Mock
def test_user_registration():
# Создаем mock для сервиса email
email_service_mock = Mock()
# Настраиваем поведение
email_service_mock.send_welcome_email.return_value = True
# Тестируем
user_service = UserService(email_service_mock)
user_service.register_user("john@example.com")
# ПРОВЕРЯЕМ, что метод был вызван
email_service_mock.send_welcome_email.assert_called_once_with("john@example.com")
Ключевые различия
| Аспект | Stub | Mock |
|---|---|---|
| Цель | Предоставить данные | Проверить взаимодействия |
| Поведение | Пассивный | Активный |
| Проверки | Не делает проверок | Проверяет вызовы методов |
| Сложность | Простой | Более сложный |
| Использование | Для состояний | Для поведения |
Статья на хабре Моки, стабы и фейки: в чем разница и когда что использовать?
Monkey patching
Monkey patching — это техника изменения поведения кода во время выполнения путем динамической замены или добавления методов, или атрибутов в существующем объекте. Эта техника может быть полезна в том случае, когда изменения не могут быть внесены в существующий код, и требует минимальных изменений в существующем коде.
Например, можно добавить новый метод в класс в runtime, который наследуется от базового класса:
class MyBaseClass:
def my_method(self):
print('Hello from MyBaseClass')
def monkey_patch():
def new_method(self):
print('Hello from new_method')
MyBaseClass.my_method = new_method
monkey_patch()
obj = MyBaseClass()
obj.my_method() # выведет "Hello from new_method"
В этом примере мы добавляем новый метод new_method() в класс MyBaseClass, используя функцию monkey_patch().
После этого, вызов метода obj.my_method() выведет строку
Hello from new_method
Важно учитывать, что использование monkey patching может усложнить отладку и поддержку в будущем, поэтому следует использовать эту технику с осторожностью и только при необходимости.
Для чего нужен assert в python и в каких случаях надо избегать использования этой конструкции?
Оператор assert в Python используется для проверки некоторого условия — если условие ложно, он вызывает исключение AssertionError с необязательным сообщением. Это удобный инструмент для отладки, фиксации предположений и нахождения ошибок на этапе разработки.
Для чего нужен assert:
* Проверка предположений о состоянии программы в ходе разработки.
-
Быстрая фиксация критических условий, которые должны быть верны для корректной работы.
-
Помощь в отладке, так как выявляет нарушение инвариантов.
-
Используется в тестах (например,
pytestактивно используетassert).
Когда нужно избегать assert:
* Не стоит использовать в производственном (продакшен) коде для валидации данных и ошибок, влияющих на логику. Это связано с тем, что при запуске Python с флагом оптимизации (-O) все операторы assert игнорируются и не выполняются.
-
Не использовать для проверки пользовательского ввода или для обязательных проверок, которые должны работать всегда.
-
Не подходит для проверки условий с побочными эффектами, так как их вычисление будет пропущено при оптимизации.
-
Для обязательных проверок лучше выбрасывать стандартные исключения (например, ValueError) через явные условия.