← Назад к курсу

Популярные библиотеки для написания тестов кода на Python

Обзор популярных библиотек для тестирования

В этом уроке мы рассмотрим три самые популярные библиотеки для тестирования в Python: unittest, pytest и doctest.


1. unittest (встроенная библиотека)

unittest - стандартная библиотека Python для модульного тестирования, вдохновленная JUnit.

Основные концепции:

  • TestCase - класс, содержащий тестовые методы
  • setUp() и tearDown() - методы для подготовки и очистки
  • assert методы - для проверок

Пример использования:

# calculator.py - код для тестирования
class Calculator:
    def add(self, a, b):
        return a + b
    
    def subtract(self, a, b):
        return a - b
    
    def multiply(self, a, b):
        return a * b
    
    def divide(self, a, b):
        if b == 0:
            raise ValueError("Cannot divide by zero")
        return a / b

# test_calculator.py - тесты
import unittest
from calculator import Calculator

class TestCalculator(unittest.TestCase):
    
    def setUp(self):
        """Выполняется перед каждым тестом"""
        self.calc = Calculator()
    
    def tearDown(self):
        """Выполняется после каждого теста"""
        pass  # Здесь можно освобождать ресурсы
    
    def test_add(self):
        result = self.calc.add(2, 3)
        self.assertEqual(result, 5)
        self.assertNotEqual(result, 6)
    
    def test_subtract(self):
        result = self.calc.subtract(10, 5)
        self.assertEqual(result, 5)
    
    def test_multiply(self):
        result = self.calc.multiply(3, 4)
        self.assertEqual(result, 12)
    
    def test_divide(self):
        result = self.calc.divide(10, 2)
        self.assertEqual(result, 5)
    
    def test_divide_by_zero(self):
        with self.assertRaises(ValueError):
            self.calc.divide(10, 0)
    
    @unittest.skip("Еще не реализовано")
    def test_future_feature(self):
        self.fail("Этот тест еще не готов")

class TestAdvancedCalculator(unittest.TestCase):
    def test_add_negative_numbers(self):
        calc = Calculator()
        result = calc.add(-5, -3)
        self.assertEqual(result, -8)

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

Запуск тестов:

# Запуск всех тестов в файле
python -m unittest test_calculator.py

# Запуск с подробным выводом
python -m unittest test_calculator.py -v

# Запуск конкретного тестового класса
python -m unittest test_calculator.TestCalculator

# Запуск конкретного теста
python -m unittest test_calculator.TestCalculator.test_add

2. pytest (самая популярная библиотека)

pytest - более современная и гибкая библиотека для тестирования.

Установка:

pip install pytest

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

  • Более читаемый синтаксис
  • Автоматическое обнаружение тестов
  • Фикстуры (fixtures)
  • Параметризация тестов

Пример использования:

# test_calculator_pytest.py
import pytest
from calculator import Calculator

# Фикстура - общий ресурс для тестов
@pytest.fixture
def calculator():
    return Calculator()

# Простые тесты
def test_add(calculator):
    result = calculator.add(2, 3)
    assert result == 5

def test_subtract(calculator):
    assert calculator.subtract(10, 5) == 5

# Тест с ожидаемым исключением
def test_divide_by_zero(calculator):
    with pytest.raises(ValueError) as exc_info:
        calculator.divide(10, 0)
    
    assert str(exc_info.value) == "Cannot divide by zero"

# Параметризованный тест (запускается несколько раз с разными данными)
@pytest.mark.parametrize("a,b,expected", [
    (1, 2, 3),
    (0, 0, 0),
    (-1, 1, 0),
    (10, -5, 5),
])
def test_add_parametrized(calculator, a, b, expected):
    assert calculator.add(a, b) == expected

# Класс с тестами
class TestCalculatorClass:
    def test_multiply(self, calculator):
        assert calculator.multiply(3, 4) == 12
    
    def test_divide(self, calculator):
        assert calculator.divide(10, 2) == 5

# Маркировка тестов
@pytest.mark.slow
def test_slow_operation():
    import time
    time.sleep(2)
    assert True

@pytest.mark.skip(reason="Еще не реализовано")
def test_future_feature():
    assert False

Фикстуры с разными областями видимости:

import pytest
import tempfile
import os

@pytest.fixture(scope="session")
def database_connection():
    """Фикстура на всю сессию тестирования"""
    print("\nУстановка соединения с БД")
    connection = {"connected": True, "data": []}
    yield connection
    print("\nЗакрытие соединения с БД")
    connection["connected"] = False

@pytest.fixture(scope="function")
def temp_file():
    """Фикстура для каждого теста"""
    with tempfile.NamedTemporaryFile(delete=False, mode='w') as f:
        f.write("test data")
        temp_path = f.name
    
    yield temp_path
    
    # Очистка после теста
    if os.path.exists(temp_path):
        os.unlink(temp_path)

def test_with_temp_file(temp_file):
    assert os.path.exists(temp_file)
    with open(temp_file, 'r') as f:
        content = f.read()
    assert content == "test data"

Запуск тестов pytest:

# Запуск всех тестов в текущей директории и поддиректориях
pytest

# Запуск конкретного файла
pytest test_calculator_pytest.py

# Запуск с подробным выводом
pytest -v

# Запуск тестов по маркерам
pytest -m slow

# Пропуск тестов с маркером
pytest -m "not slow"

# Запуск с отчетом о покрытии (требуется pytest-cov)
pytest --cov=calculator

3. doctest (тестирование через документацию)

doctest позволяет писать тесты прямо в docstring.

Пример использования:

# calculator_doctest.py
def add(a, b):
    """
    Складывает два числа.
    
    >>> add(2, 3)
    5
    >>> add(-1, 1)
    0
    >>> add(0, 0)
    0
    >>> add(2.5, 3.5)
    6.0
    """
    return a + b

def divide(a, b):
    """
    Делит a на b.
    
    >>> divide(10, 2)
    5.0
    >>> divide(1, 4)
    0.25
    
    Деление на ноль вызывает исключение:
    >>> divide(10, 0)
    Traceback (most recent call last):
        ...
    ValueError: Cannot divide by zero
    """
    if b == 0:
        raise ValueError("Cannot divide by zero")
    return a / b

def fibonacci(n):
    """
    Возвращает n-ое число Фибоначчи.
    
    >>> fibonacci(0)
    0
    >>> fibonacci(1)
    1
    >>> fibonacci(2)
    1
    >>> fibonacci(5)
    5
    >>> fibonacci(10)
    55
    
    Для отрицательных чисел:
    >>> fibonacci(-1)
    Traceback (most recent call last):
        ...
    ValueError: n must be non-negative
    """
    if n < 0:
        raise ValueError("n must be non-negative")
    
    if n <= 1:
        return n
    
    a, b = 0, 1
    for _ in range(2, n + 1):
        a, b = b, a + b
    return b

if __name__ == "__main__":
    import doctest
    # Запуск тестов
    doctest.testmod(verbose=True)

Запуск doctest:

# Запуск из командной строки
python -m doctest calculator_doctest.py

# С подробным выводом
python -m doctest calculator_doctest.py -v

# Запуск напрямую из файла
python calculator_doctest.py

Расширенный пример с комбинированием:

# advanced_doctest.py
"""
Модуль с математическими функциями.

Примеры использования:
>>> from advanced_doctest import MathOperations
>>> calc = MathOperations()
>>> calc.power(2, 3)
8
>>> calc.is_even(4)
True
"""

class MathOperations:
    """Класс для математических операций."""
    
    def power(self, base, exponent):
        """
        Возводит base в степень exponent.
        
        >>> m = MathOperations()
        >>> m.power(2, 3)
        8
        >>> m.power(5, 0)
        1
        >>> m.power(2, -1)
        0.5
        """
        return base ** exponent
    
    def is_even(self, number):
        """
        Проверяет, является ли число четным.
        
        >>> m = MathOperations()
        >>> m.is_even(4)
        True
        >>> m.is_even(7)
        False
        >>> m.is_even(0)
        True
        """
        return number % 2 == 0
    
    def factorial(self, n):
        """
        Вычисляет факториал числа n.
        
        >>> m = MathOperations()
        >>> m.factorial(0)
        1
        >>> m.factorial(1)
        1
        >>> m.factorial(5)
        120
        
        Для отрицательных чисел:
        >>> m.factorial(-1)
        Traceback (most recent call last):
            ...
        ValueError: n must be non-negative
        """
        if n < 0:
            raise ValueError("n must be non-negative")
        
        result = 1
        for i in range(2, n + 1):
            result *= i
        return result

if __name__ == "__main__":
    import doctest
    doctest.testmod()

Сравнение библиотек

Критерий unittest pytest doctest
Сложность Средняя Низкая Очень низкая
Гибкость Средняя Высокая Низкая
Встроен в Python Да Нет Да
Читаемость Средняя Высокая Высокая
Фикстуры Есть Очень мощные Нет
Параметризация Через subTest Отличная Нет

Практические рекомендации

1. Организация тестов:

project/
├── src/
│   ├── __init__.py
│   ├── calculator.py
│   └── utils.py
├── tests/
│   ├── __init__.py
│   ├── test_calculator.py
│   ├── test_utils.py
│   └── conftest.py  # общие фикстуры для pytest
├── requirements.txt
└── setup.py

2. conftest.py для общих фикстур:

# tests/conftest.py
import pytest
import json
import tempfile

@pytest.fixture
def sample_config():
    return {
        "api_key": "test_key",
        "timeout": 30,
        "retries": 3
    }

@pytest.fixture
def config_file(sample_config):
    with tempfile.NamedTemporaryFile(mode='w', suffix='.json', delete=False) as f:
        json.dump(sample_config, f)
        temp_path = f.name
    
    yield temp_path
    
    import os
    if os.path.exists(temp_path):
        os.unlink(temp_path)

3. Интеграция с CI/CD (GitHub Actions):

# .github/workflows/tests.yml
name: Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    
    steps:
    - uses: actions/checkout@v2
    
    - name: Set up Python
      uses: actions/setup-python@v2
      with:
        python-version: '3.9'
    
    - name: Install dependencies
      run: |
        python -m pip install --upgrade pip
        pip install pytest pytest-cov
        pip install -r requirements.txt
    
    - name: Run tests
      run: |
        pytest --cov=src --cov-report=xml
    
    - name: Upload coverage
      uses: codecov/codecov-action@v2

4. Покрытие кода (coverage):

# Установка
pip install pytest-cov

# Запуск с измерением покрытия
pytest --cov=src --cov-report=html

# Минимальное покрытие
pytest --cov=src --cov-fail-under=80

Заключение

  1. unittest - хорош для проектов, где нужна совместимость со стандартной библиотекой
  2. pytest - лучший выбор для большинства проектов, богатые возможности
  3. doctest - отлично подходит для документации и простых модулей

Для начала рекомендую использовать pytest из-за его простоты и мощи. По мере роста проекта вы сможете использовать все его продвинутые функции, такие как фикстуры, параметризация и плагины.

Лучшие практики:

  • Пишите тесты до или параллельно с кодом (TDD)
  • Тестируйте как успешные сценарии, так и ошибки
  • Держите тесты независимыми
  • Используйте понятные имена для тестов
  • Регулярно запускайте тесты в CI/CD