Skip to content

Тестирование

Чем 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) через явные условия.