Основы тестирования кода: unittest и pytest

Почему тестирование — это круто? 🧪

Представь: ты написал сложную функцию, она работает... вроде бы. Но вдруг в продакшене ломается из-за неожиданных данных. Тестирование — это твой страховочный трос! Оно:

  • Ускоряет разработку (меньше дебага)
  • Делает код надежнее
  • Позволяет рефакторить без страха

В Python два главных инструмента: unittest (из коробки) и pytest (гибкость и мощь).


unittest — стандартный подход

Базовый пример

import unittest

def add(a, b):
    return a + b

class TestMath(unittest.TestCase):
    def test_add(self):
        self.assertEqual(add(2, 3), 5)  # Проверка равенства
        self.assertNotEqual(add(2, 2), 5)  # Проверка неравенства

    def test_add_negative(self):
        self.assertEqual(add(-1, -1), -2)

if __name__ == '__main__':
    unittest.main()

Разберем по шагам:

  1. Создаем класс, наследующий unittest.TestCase
  2. Каждый тест — метод, начинающийся с test_
  3. Используем assert-методы (assertEqual, assertTrue и др.)
  4. Запускаем через unittest.main()

Фикстуры: подготовка и очистка

class TestDatabase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        # Выполняется 1 раз для всех тестов
        cls.db = connect_to_test_db()

    def setUp(self):
        # Выполняется перед КАЖДЫМ тестом
        self.db.clear_tables()

    def test_insert(self):
        self.db.insert('data')
        self.assertTrue(self.db.contains('data'))

    def tearDown(self):
        # Выполняется после КАЖДОГО теста
        self.db.rollback()

Когда использовать:

  • setUpClass — дорогие операции (например, подключение к БД)
  • setUp/tearDown — подготовка контекста для каждого теста

pytest — магия простоты

Основные преимущества

  • Нет необходимости в классах
  • Подробные сообщения об ошибках
  • Фикстуры с декларативным синтаксисом
  • Плагины (например, pytest-cov для покрытия кода)

Пример теста

# test_math.py
def multiply(a, b):
    return a * b

def test_multiply():
    assert multiply(3, 4) == 12
    assert multiply(0, 10) == 0

Запуск: pytest test_math.py -v (-v для подробного вывода)


Мощные фикстуры pytest

import pytest

@pytest.fixture
def database():
    db = connect_to_db()
    yield db  # Это точка, где тест получает объект
    db.disconnect()  # Выполнится после теста

def test_query(database):  # Фикстура передается как аргумент!
    result = database.query("SELECT 1")
    assert result == 1

Почему это круто:

  • Фикстуры можно переиспользовать
  • Автоматическая очистка через yield
  • Можно делать область видимости (@pytest.fixture(scope="module"))

Параметризация: тестируем все случаи

import pytest

@pytest.mark.parametrize("a, b, expected", [
    (2, 3, 5),
    (0, 0, 0),
    (-1, 1, 0),
])
def test_add(a, b, expected):
    assert add(a, b) == expected

Что происходит:

  • pytest запустит тест 3 раза с разными параметрами
  • Если один случай упадет — остальные проверятся
  • Идеально для edge-кейсов!

Что выбрать: unittest или pytest?

Критерий unittest pytest
Сложность Проще для новичков Более гибкий
Фикстуры Классовые методы Декларативные
Параметризация Через subTest Встроенная поддержка
Интеграция В стандартной библиотеке Требует установки

Совет от Данилы Бежина: Начинайте с unittest, затем переходите на pytest для сложных проектов. Подробнее — в его видео.


Практический лайфхак: покрытие тестами

Установите pytest-cov:

pip install pytest-cov

Запустите с проверкой покрытия:

pytest --cov=my_project tests/

Вы увидите процент кода, который покрыт тестами. Цель — 70-90%, не стремитесь к 100% там, где это неоправданно!

Name              Stmts   Miss  Cover
-------------------------------------
my_project/math.py     10      2    80%
Скрыть рекламу навсегда

🧠 Учёба без воды и зубрёжки

Закрытый Boosty с наработками опытного преподавателя.

Объясняю сложное так, чтобы щелкнуло.

🚀 Забрать доступ к Boosty