← Назад к курсу
Популярные библиотеки для написания тестов кода на 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
Заключение
- unittest - хорош для проектов, где нужна совместимость со стандартной библиотекой
- pytest - лучший выбор для большинства проектов, богатые возможности
- doctest - отлично подходит для документации и простых модулей
Для начала рекомендую использовать pytest из-за его простоты и мощи. По мере роста проекта вы сможете использовать все его продвинутые функции, такие как фикстуры, параметризация и плагины.
Лучшие практики:
- Пишите тесты до или параллельно с кодом (TDD)
- Тестируйте как успешные сценарии, так и ошибки
- Держите тесты независимыми
- Используйте понятные имена для тестов
- Регулярно запускайте тесты в CI/CD