← Назад к вопросам
Какие знаешь методы для unit-тестирования?
1.0 Junior🔥 191 комментариев
#Тестирование
Комментарии (1)
🐱
claude-haiku-4.5PrepBro AI22 мар. 2026 г.(ред.)
Ответ сгенерирован нейросетью и может содержать ошибки
Методы для unit-тестирования в Python
Unit-тестирование — это тестирование отдельных компонентов кода в изоляции. Расскажу о методах, инструментах и best practices, которые использую в работе.
1. Основные фреймворки
pytest (рекомендуемый)
Модерный, гибкий, с отличными возможностями расширения.
# test_calculator.py
import pytest
def add(a, b):
return a + b
# Простой тест
def test_add_positive_numbers():
assert add(2, 3) == 5
# Параметризованный тест
@pytest.mark.parametrize("a,b,expected", [
(2, 3, 5),
(-1, 1, 0),
(0, 0, 0),
])
def test_add_various(a, b, expected):
assert add(a, b) == expected
# Тест на исключение
def test_add_invalid_input():
with pytest.raises(TypeError):
add("a", 1)
# Фикстуры
@pytest.fixture
def sample_data():
return {"a": 2, "b": 3}
def test_with_fixture(sample_data):
assert add(sample_data["a"], sample_data["b"]) == 5
Запуск:
# Все тесты
pytest
# Конкретный файл
pytest test_calculator.py
# С verbose и coverage
pytest -v --cov=myapp
# В watch mode
pytest-watch
unittest (встроенный)
Входит в стандартную библиотеку Python. Более формальный стиль.
import unittest
class TestCalculator(unittest.TestCase):
def setUp(self):
"""Выполняется перед каждым тестом"""
self.calculator = Calculator()
def tearDown(self):
"""Выполняется после каждого теста"""
self.calculator = None
def test_add(self):
result = self.calculator.add(2, 3)
self.assertEqual(result, 5)
def test_divide_by_zero(self):
with self.assertRaises(ValueError):
self.calculator.divide(5, 0)
if __name__ == '__main__':
unittest.main()
2. Mocking и Patching
Изоляция компонента от внешних зависимостей.
Mock объекты
from unittest.mock import Mock, MagicMock, patch
# Mock базовый
def test_with_mock():
mock_db = Mock()
mock_db.get_user.return_value = {"id": 1, "name": "Alice"}
user = mock_db.get_user(1)
assert user["name"] == "Alice"
mock_db.get_user.assert_called_once_with(1)
# MagicMock — более функциональный
def test_with_magic_mock():
mock_api = MagicMock()
mock_api.fetch_data.return_value = [{"id": 1}]
# Можем проверить аргументы
assert mock_api.fetch_data.call_count >= 0
mock_api.fetch_data.assert_called_with()
Patch декоратор
from unittest.mock import patch
# Патчим на уровне модуля
@patch('requests.get')
def test_fetch_data(mock_get):
mock_get.return_value.status_code = 200
mock_get.return_value.json.return_value = {"id": 1}
# Здесь requests.get — это mock
from mymodule import fetch_user
result = fetch_user(1)
assert result["id"] == 1
mock_get.assert_called_once()
# Context manager для временного патча
def test_with_context_manager():
with patch('builtins.open', create=True) as mock_open:
mock_open.return_value.__enter__.return_value.read.return_value = "data"
# код, использующий open()
pytest-mock (рекомендуется)
def test_with_pytest_mock(mocker):
# Проще синтаксис
mock_api = mocker.MagicMock()
mock_api.get_data.return_value = {"status": "ok"}
# Патчим функцию
mocker.patch('mymodule.external_api', mock_api)
result = external_api.get_data()
assert result["status"] == "ok"
3. Работа с БД в тестах
Использование fixtures
@pytest.fixture
def db_session():
"""Создаёт сессию для каждого теста"""
session = create_session()
yield session
session.rollback() # Откатываем изменения
@pytest.fixture
def user_factory():
"""Factory для создания тестовых пользователей"""
def _create_user(name="Test User", email="test@example.com"):
return User(name=name, email=email)
return _create_user
def test_user_creation(db_session, user_factory):
user = user_factory(name="Alice")
db_session.add(user)
db_session.commit()
fetched = db_session.query(User).filter_by(name="Alice").first()
assert fetched.email == "test@example.com"
Тестирование с реальной БД (рекомендуется для интеграционных тестов)
import pytest
from django.test import TestCase
class UserRepositoryTest(TestCase):
@classmethod
def setUpClass(cls):
"""Выполняется один раз перед всеми тестами класса"""
super().setUpClass()
# Миграции уже запущены Django
def setUp(self):
"""Выполняется перед каждым тестом"""
self.user = User.objects.create(name="Alice", email="alice@example.com")
def test_user_exists(self):
user = User.objects.get(id=self.user.id)
assert user.name == "Alice"
4. Тестирование асинхронного кода
asyncio
import pytest
import asyncio
@pytest.mark.asyncio
async def test_async_function():
result = await async_operation()
assert result == expected_value
@pytest.fixture
def event_loop():
"""Для pytest-asyncio"""
loop = asyncio.get_event_loop_policy().new_event_loop()
yield loop
loop.close()
Тестирование Telegram ботов
from aiogram.dispatcher import Dispatcher
from aiogram.types import Update, User as TgUser, Chat, Message
from aiogram.types import CallbackQuery
@pytest.mark.asyncio
async def test_start_handler(dp: Dispatcher):
# Создаём fake update
message = Message(
message_id=1,
date=0,
chat=Chat(id=123, type="private"),
from_user=TgUser(id=123, is_bot=False, first_name="Test")
)
message.text = "/start"
update = Update(update_id=1, message=message)
# Отправляем update в dispatcher
await dp.feed_update(dp.bot, update)
# Проверяем результат
5. Test-Driven Development (TDD)
Пишу тест ДО кода:
# Шаг 1: RED — пишу падающий тест
def test_user_can_change_password():
user = User(password="old_password")
user.change_password("old_password", "new_password")
assert user.check_password("new_password")
# Шаг 2: GREEN — пишу минимальный код
class User:
def __init__(self, password):
self.password = password
def change_password(self, old, new):
if self.check_password(old):
self.password = new
def check_password(self, pwd):
return self.password == pwd
# Шаг 3: REFACTOR — улучшаю код
from werkzeug.security import generate_password_hash, check_password_hash
class User:
def __init__(self, password):
self.password_hash = generate_password_hash(password)
def change_password(self, old, new):
if self.check_password(old):
self.password_hash = generate_password_hash(new)
def check_password(self, pwd):
return check_password_hash(self.password_hash, pwd)
6. Покрытие тестами (Coverage)
# Запустить с отчётом о покрытии
pytest --cov=myapp --cov-report=html
# Проверить конкретные файлы
pytest --cov=myapp.models test_models.py
# В коде: пропустить строку если нужно
def debug_only(): # pragma: no cover
print("Debug info")
7. Тестирование исключений
# pytest
def test_error_handling():
with pytest.raises(ValueError, match="Invalid input"):
process_data(invalid_data)
# unittest
def test_error_handling(self):
self.assertRaises(ValueError, process_data, invalid_data)
# или
with self.assertRaises(ValueError):
process_data(invalid_data)
8. Fixtures для комплексных сценариев
@pytest.fixture
def authenticated_client(client):
"""Клиент с авторизованным пользователем"""
user = User.objects.create(username="testuser", password="testpass")
client.force_login(user)
return client
@pytest.fixture
def api_client():
"""REST API клиент"""
from rest_framework.test import APIClient
return APIClient()
@pytest.fixture(params=["en", "ru", "de"])
def language(request):
"""Параметризованная фикстура для разных языков"""
return request.param
def test_multilingual(language):
# Тест запустится 3 раза с разными языками
pass
9. Лучшие практики
# 1. Один assert на тест (желательно)
def test_user_creation():
user = User.create(name="Alice")
assert user.name == "Alice"
# 2. Использовать descriptive names
def test_user_cannot_login_with_wrong_password():
pass
# 3. Arrange-Act-Assert
def test_discount_calculation():
# Arrange
order = Order(total=100)
discount = VipDiscount()
# Act
result = discount.apply(order)
# Assert
assert result == 90 # 10% discount
# 4. Избегать флакИ тестов
# ПЛОХО: зависит от времени
def test_time_based():
current = datetime.now().hour
assert current >= 0 # Может быть false в некоторых случаях
# ХОРОШО: мокируем время
@patch('mymodule.datetime')
def test_time_based(mock_datetime):
mock_datetime.now.return_value = datetime(2024, 1, 1, 12, 0)
# Тест всегда deterministic
10. Примеры реальных тестов
# Django ORM тест
def test_user_repository_find_by_email(db):
user = User.objects.create(name="Alice", email="alice@example.com")
found = User.objects.get(email="alice@example.com")
assert found.id == user.id
# API тест
def test_get_user_endpoint(api_client):
user = User.objects.create(name="Alice", email="alice@example.com")
response = api_client.get(f"/api/users/{user.id}/")
assert response.status_code == 200
assert response.data["name"] == "Alice"
# Service тест с mocks
def test_user_service_send_welcome_email(mocker):
mock_email = mocker.patch('myapp.services.send_email')
service = UserService()
service.register_user("alice@example.com")
mock_email.assert_called_once()
args = mock_email.call_args
assert "welcome" in args[1]["subject"].lower()
Вывод
Лучший набор инструментов для unit-тестирования:
- pytest как основной фреймворк
- pytest-mock для мокирования
- pytest-cov для проверки покрытия
- pytest-asyncio для асинхронного кода
- TDD подход для качественного кода
- Mock объекты для изоляции компонентов
- Минимум 90% покрытия для production кода
Это обеспечивает надёжность, поддерживаемость и уверенность в коде.