Тестирование Python программ: выбор инструмента и шаблоны
Основные подходы к тестированию Python кода
Тестирование кода на Python охватывает проверку отдельных функций, классов и интеграции модулей. Основная цель – обеспечить корректную работу программы при изменениях и предотвратить регрессии. Рассмотрим наиболее эффективные инструменты и их применение.
Как автоматизировать тестирование с помощью pytest?
pytest – современный фреймворк, который требует минимум кода и предоставляет мощные возможности: автоматическое обнаружение тестов, фикстуры, параметризацию, плагины.
Установка:
pip install pytestPython тест программ (тестирование программ python)
Пример теста для функции вычисления факториала. Создайте файл test_math.py:
# test_math.py
def factorial(n):
if n == 0:
return 1
return n * factorial(n-1)
def test_factorial_positive():
assert factorial(5) == 120
def test_factorial_zero():
assert factorial(0) == 1
Запуск:
pytest test_math.py
Результат:
============================= test session starts ============================== collected 2 items test_math.py .. [100%] ============================== 2 passed in 0.02s ===============================
Для параметризации тестов используйте декоратор @pytest.mark.parametrize:
import pytest
def factorial(n):
if n < 0:
raise ValueError("Отрицательное значение")
if n == 0:
return 1
return n * factorial(n-1)
@pytest.mark.parametrize("n,expected", [
(0, 1),
(1, 1),
(5, 120),
(10, 3628800),
])
def test_factorial(n, expected):
assert factorial(n) == expected
@pytest.mark.parametrize("n", [-1, -10])
def test_factorial_negative(n):
with pytest.raises(ValueError):
factorial(n)
Фикстуры позволяют подготовить общие данные. Пример с временным файлом:
import pytest
@pytest.fixture
def temp_file(tmp_path):
file = tmp_path / "data.txt"
file.write_text("test content")
return file
def test_read_temp_file(temp_file):
data = temp_file.read_text()
assert data == "test content"
Типичная ошибка: забыть установить pytest в окружение. Используйте виртуальное окружение или pip install pytest.
Также тесты должны начинаться с test_ или оканчиваться на _test для обнаружения.
Проблема с областью видимости фикстуры – по умолчанию function, что приводит к повторному созданию для каждого теста. Для ресурсоёмких объектов используйте scope="module" или scope="session".
Как использовать unittest для написания тестов?
Модуль unittest встроен в Python, основан на JUnit. Он требует создания классов-наследников TestCase и определения методов с именем, начинающимся на test.
import unittest
def add(a, b):
return a + b
class TestMath(unittest.TestCase):
def test_add_positive(self):
self.assertEqual(add(2, 3), 5)
def test_add_negative(self):
self.assertEqual(add(-1, -1), -2)
if __name__ == "__main__":
unittest.main()
Запуск:
python -m unittest test_math.py
Недостаток: больше шаблонного кода, менее лаконичные утверждения.
Как протестировать код с помощью doctest?
Doctest позволяет встроить тесты прямо в документацию функции. Примеры копируются из docstring и проверяются.
def multiply(a, b):
"""
Умножает два числа.
>>> multiply(2, 3)
6
>>> multiply(10, 0)
0
>>> multiply(-1, 5)
-5
"""
return a * b
if __name__ == "__main__":
import doctest
doctest.testmod()
Запуск:
python -m doctest test_doctest.py -v
Подходит для простых проверок, но не для сложной логики и внешних зависимостей.
Ошибка: doctest чувствителен к пробелам и ожидает точное совпадение вывода. Лучше не использовать для больших проектов.
При использовании unittest частая проблема – забыть вызвать super().setUp() в переопределённом методе setUp, что может нарушить работу тестов.
Как выполнить временное тестирование (перехват вывода)?
pytest предоставляет фикстуру capsys для захвата stdout и stderr. Пример:
def greet(name):
print(f"Hello, {name}!")
def test_greet(capsys):
greet("Alice")
captured = capsys.readouterr()
assert captured.out == "Hello, Alice!\n"
Как mock внешние вызовы?
Использование unittest.mock или pytest-mock. Пример с запросом HTTP:
import requests
def get_user_data(user_id):
response = requests.get(f"https://api.example.com/users/{user_id}")
return response.json()
# test with mock
def test_get_user_data(mocker):
mock_response = mocker.Mock()
mock_response.json.return_value = {"id": 1, "name": "Alice"}
mocker.patch('requests.get', return_value=mock_response)
result = get_user_data(1)
assert result == {"id": 1, "name": "Alice"}
Проблема: неправильный путь для patching – нужно указывать полный путь в импорте модуля, где используется объект.
Расширенные примеры тестирования Python кода
Тестирование свойств с помощью Hypothesis
Hypothesis генерирует множество случайных входных данных для поиска граничных случаев. Установка:
pip install hypothesis
from hypothesis import given, strategies as st
from mymodule import encode, decode
@given(st.text())
def test_encode_decode(text):
encoded = encode(text)
decoded = decode(encoded)
assert decoded == text
Запуск:
hypothesis test_example.py::test_encode_decode ... found no counterexample (seems valid)
Важно: тесты могут быть медленными, ограничивайте стратегии параметрами max_examples.
Интеграционное тестирование Flask приложения
Используйте клиент тестирования Flask:
# app.py
from flask import Flask
app = Flask(__name__)
@app.route('/')
def index():
return 'Hello, World!'
# test_app.py
import pytest
from app import app
@pytest.fixture
def client():
with app.test_client() as client:
yield client
def test_index(client):
response = client.get('/')
assert response.status_code == 200
assert b'Hello, World!' in response.data
Результат:
pytest test_app.py -v ================== test session starts ================== test_app.py::test_index PASSED
Ошибка: не забыть импортировать приложение или использовать фабрику.
Тестирование с покрытием кода (coverage)
Установка:
pip install pytest-cov
# запуск с отчётом
pytest --cov=myproject --cov-report=html tests/
Результат – папка htmlcov с детальным отчётом. Позволяет найти не покрытые тестами строки.
Параметризация с внешними данными (JSON, CSV)
import json
import pytest
@pytest.fixture
def test_data():
with open('tests/test_data.json') as f:
return json.load(f)
def test_from_json(test_data):
for entry in test_data:
assert entry['expected'] == myfunc(entry['input'])
Можно использовать @pytest.mark.parametrize с загрузкой списка.
Тестирование исключений и флагов
import pytest
def set_age(age):
if age < 0:
raise ValueError("Age cannot be negative")
return age
def test_set_age_negative():
with pytest.raises(ValueError, match="Age cannot be negative"):
set_age(-1)
def test_set_age_valid():
assert set_age(25) == 25
Проверка точного сообщения об ошибке важна для документации.