- Home »

Python unittest — пример юнит-теста и руководство
Когда твой сервер начинает ломаться от собственных скриптов, а багфиксы превращаются в игру “поймай меня, если сможешь”, пора задуматься о тестировании. Сегодня разберём Python unittest — стандартный инструмент для написания юнит-тестов, который поможет тебе ловить баги раньше, чем они доберутся до продакшена. Особенно это актуально для серверных скриптов, автоматизации и API, где каждый сбой может стоить дорого.
Unittest — это не просто “хорошая практика”, это твоя страховка от того момента, когда в 3 утра тебе звонят с вопросом “а почему сервер не отвечает?”. Мы пройдёмся по основам, посмотрим на практические примеры и разберём, как интегрировать тесты в твой workflow.
Как работает unittest — основы и принципы
Python unittest работает по принципу xUnit — паттерна, который используется в большинстве языков программирования. Основная идея: создаёшь класс, наследующийся от unittest.TestCase, пишешь методы, начинающиеся с test_, и фреймворк автоматически их находит и запускает.
Основные компоненты unittest:
- TestCase — базовый класс для всех тестов
- TestSuite — группа тестов
- TestRunner — запускает тесты и собирает результаты
- Test fixtures — setUp() и tearDown() для подготовки и очистки
Вот простейший пример:
import unittest
def add_numbers(a, b):
return a + b
class TestAddNumbers(unittest.TestCase):
def test_add_positive_numbers(self):
result = add_numbers(2, 3)
self.assertEqual(result, 5)
def test_add_negative_numbers(self):
result = add_numbers(-1, -1)
self.assertEqual(result, -2)
def test_add_zero(self):
result = add_numbers(5, 0)
self.assertEqual(result, 5)
if __name__ == '__main__':
unittest.main()
Быстрая настройка и первые шаги
Unittest идёт в комплекте с Python, поэтому дополнительных установок не требуется. Но для серверных задач лучше создать отдельную директорию для тестов:
# Создаём структуру проекта
mkdir my_server_project
cd my_server_project
mkdir tests
touch tests/__init__.py
# Создаём основной модуль
touch server_utils.py
# Создаём тест
touch tests/test_server_utils.py
Пример серверного модуля с функциями для тестирования:
# server_utils.py
import subprocess
import psutil
import requests
def check_port_open(port):
"""Проверяет, открыт ли порт"""
try:
result = subprocess.run(['netstat', '-tuln'],
capture_output=True, text=True)
return f":{port}" in result.stdout
except Exception:
return False
def get_cpu_usage():
"""Возвращает использование CPU"""
return psutil.cpu_percent(interval=1)
def ping_service(url, timeout=5):
"""Проверяет доступность сервиса"""
try:
response = requests.get(url, timeout=timeout)
return response.status_code == 200
except:
return False
def parse_log_line(line):
"""Парсит строку лога"""
parts = line.split(' - ')
if len(parts) >= 3:
return {
'timestamp': parts[0],
'level': parts[1],
'message': parts[2]
}
return None
Теперь создаём тесты:
# tests/test_server_utils.py
import unittest
from unittest.mock import patch, MagicMock
import sys
import os
# Добавляем родительскую директорию в path
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from server_utils import check_port_open, get_cpu_usage, ping_service, parse_log_line
class TestServerUtils(unittest.TestCase):
def setUp(self):
"""Выполняется перед каждым тестом"""
self.sample_log_line = "2024-01-15 10:30:45 - INFO - Server started"
self.invalid_log_line = "Invalid log format"
def tearDown(self):
"""Выполняется после каждого теста"""
pass
@patch('subprocess.run')
def test_check_port_open_success(self, mock_run):
"""Тест проверки открытого порта"""
mock_run.return_value.stdout = "tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN"
result = check_port_open(80)
self.assertTrue(result)
@patch('subprocess.run')
def test_check_port_open_closed(self, mock_run):
"""Тест проверки закрытого порта"""
mock_run.return_value.stdout = "tcp 0 0 0.0.0.0:22 0.0.0.0:* LISTEN"
result = check_port_open(80)
self.assertFalse(result)
@patch('psutil.cpu_percent')
def test_get_cpu_usage(self, mock_cpu):
"""Тест получения CPU usage"""
mock_cpu.return_value = 25.5
result = get_cpu_usage()
self.assertEqual(result, 25.5)
mock_cpu.assert_called_once_with(interval=1)
@patch('requests.get')
def test_ping_service_success(self, mock_get):
"""Тест успешного пинга сервиса"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_get.return_value = mock_response
result = ping_service("http://example.com")
self.assertTrue(result)
@patch('requests.get')
def test_ping_service_failure(self, mock_get):
"""Тест неуспешного пинга сервиса"""
mock_get.side_effect = Exception("Connection error")
result = ping_service("http://nonexistent.com")
self.assertFalse(result)
def test_parse_log_line_valid(self):
"""Тест парсинга корректной строки лога"""
result = parse_log_line(self.sample_log_line)
expected = {
'timestamp': '2024-01-15 10:30:45',
'level': 'INFO',
'message': 'Server started'
}
self.assertEqual(result, expected)
def test_parse_log_line_invalid(self):
"""Тест парсинга некорректной строки лога"""
result = parse_log_line(self.invalid_log_line)
self.assertIsNone(result)
if __name__ == '__main__':
unittest.main()
Запуск тестов и интерпретация результатов
Есть несколько способов запустить тесты:
# Запуск конкретного файла
python -m unittest tests.test_server_utils
# Запуск всех тестов в директории
python -m unittest discover tests
# Запуск с подробным выводом
python -m unittest -v tests.test_server_utils
# Запуск конкретного теста
python -m unittest tests.test_server_utils.TestServerUtils.test_parse_log_line_valid
Для автоматизации на сервере создай скрипт:
# run_tests.sh
#!/bin/bash
echo "Running unit tests..."
python -m unittest discover tests -v
if [ $? -eq 0 ]; then
echo "All tests passed!"
exit 0
else
echo "Tests failed!"
exit 1
fi
Продвинутые возможности и практические кейсы
Для серверных задач часто нужны более сложные сценарии. Вот несколько полезных паттернов:
Тестирование с базами данных
import unittest
import sqlite3
import tempfile
import os
class TestDatabaseOperations(unittest.TestCase):
def setUp(self):
"""Создаём временную БД для каждого теста"""
self.db_fd, self.db_path = tempfile.mkstemp()
self.conn = sqlite3.connect(self.db_path)
self.conn.execute('''
CREATE TABLE users (
id INTEGER PRIMARY KEY,
username TEXT NOT NULL,
email TEXT NOT NULL
)
''')
self.conn.commit()
def tearDown(self):
"""Удаляем временную БД"""
self.conn.close()
os.close(self.db_fd)
os.unlink(self.db_path)
def test_user_creation(self):
cursor = self.conn.cursor()
cursor.execute("INSERT INTO users (username, email) VALUES (?, ?)",
("testuser", "test@example.com"))
self.conn.commit()
cursor.execute("SELECT * FROM users WHERE username = ?", ("testuser",))
user = cursor.fetchone()
self.assertIsNotNone(user)
self.assertEqual(user[1], "testuser")
Тестирование API эндпоинтов
import unittest
from unittest.mock import patch, MagicMock
import json
class TestAPIEndpoints(unittest.TestCase):
@patch('requests.post')
def test_api_authentication(self, mock_post):
"""Тест аутентификации API"""
mock_response = MagicMock()
mock_response.status_code = 200
mock_response.json.return_value = {"token": "abc123", "expires_in": 3600}
mock_post.return_value = mock_response
# Здесь был бы вызов твоей функции аутентификации
# result = authenticate_api("user", "pass")
# self.assertEqual(result["token"], "abc123")
def test_validate_config(self):
"""Тест валидации конфигурации"""
valid_config = {
"server_port": 8080,
"debug": False,
"database_url": "sqlite:///test.db"
}
# Здесь была бы твоя функция валидации
# self.assertTrue(validate_server_config(valid_config))
invalid_config = {
"server_port": "invalid",
"debug": "not_boolean"
}
# self.assertFalse(validate_server_config(invalid_config))
Assertion методы и их использование
Unittest предоставляет множество assertion методов. Вот самые полезные для серверных задач:
Метод | Описание | Пример использования |
---|---|---|
assertEqual(a, b) | Проверяет равенство | self.assertEqual(status_code, 200) |
assertNotEqual(a, b) | Проверяет неравенство | self.assertNotEqual(error_count, 0) |
assertTrue(x) | Проверяет истинность | self.assertTrue(is_service_running()) |
assertFalse(x) | Проверяет ложность | self.assertFalse(has_errors) |
assertIsNone(x) | Проверяет на None | self.assertIsNone(get_user_by_id(999)) |
assertIn(a, b) | Проверяет вхождение | self.assertIn(“ERROR”, log_content) |
assertRaises(exception) | Проверяет исключение | self.assertRaises(ValueError, invalid_func) |
Интеграция с CI/CD и автоматизация
Для полноценной автоматизации создай конфигурацию для GitHub Actions:
# .github/workflows/tests.yml
name: Run Tests
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: [3.8, 3.9, '3.10']
steps:
- uses: actions/checkout@v2
- name: Set up Python ${{ matrix.python-version }}
uses: actions/setup-python@v2
with:
python-version: ${{ matrix.python-version }}
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install -r requirements.txt
- name: Run tests
run: |
python -m unittest discover tests -v
- name: Generate coverage report
run: |
pip install coverage
coverage run -m unittest discover tests
coverage report -m
Для серверов можно настроить pre-commit hook:
# .git/hooks/pre-commit
#!/bin/bash
echo "Running tests before commit..."
python -m unittest discover tests
if [ $? -ne 0 ]; then
echo "Tests failed. Commit aborted."
exit 1
fi
echo "All tests passed. Proceeding with commit."
Сравнение с альтернативными решениями
Фреймворк | Преимущества | Недостатки | Когда использовать |
---|---|---|---|
unittest | Встроенный, стандартный, богатый API | Многословный, сложный синтаксис | Корпоративные проекты, стандартизация |
pytest | Простой синтаксис, мощные fixtures | Дополнительная зависимость | Быстрая разработка, современные проекты |
nose2 | Расширяемый, совместим с unittest | Меньше сообщество | Миграция с nose |
doctest | Документация + тесты | Ограниченные возможности | Простые функции, примеры в доках |
Мониторинг и отчётность
Для серверных задач важно собирать метрики тестов. Создай скрипт для генерации отчётов:
# test_reporter.py
import unittest
import sys
import json
import time
from io import StringIO
class TestReporter:
def __init__(self):
self.results = []
def run_tests_with_report(self, test_module):
"""Запускает тесты и генерирует отчёт"""
# Перехватываем stdout
old_stdout = sys.stdout
sys.stdout = captured_output = StringIO()
# Запускаем тесты
loader = unittest.TestLoader()
suite = loader.loadTestsFromModule(test_module)
start_time = time.time()
runner = unittest.TextTestRunner(stream=captured_output, verbosity=2)
result = runner.run(suite)
end_time = time.time()
# Восстанавливаем stdout
sys.stdout = old_stdout
# Формируем отчёт
report = {
"timestamp": time.strftime("%Y-%m-%d %H:%M:%S"),
"total_tests": result.testsRun,
"failures": len(result.failures),
"errors": len(result.errors),
"duration": round(end_time - start_time, 2),
"success_rate": round((result.testsRun - len(result.failures) - len(result.errors)) / result.testsRun * 100, 2) if result.testsRun > 0 else 0
}
return report, captured_output.getvalue()
# Использование
if __name__ == '__main__':
reporter = TestReporter()
# report, output = reporter.run_tests_with_report(test_module)
# print(json.dumps(report, indent=2))
Оптимизация производительности тестов
Для больших серверных проектов производительность тестов критична. Несколько советов:
- Используй setUp/tearDown осторожно — они выполняются для каждого теста
- setUpClass/tearDownClass — для инициализации всего класса
- Мокай внешние зависимости — база данных, API, файловая система
- Группируй тесты логически — быстрые unit-тесты отдельно от медленных интеграционных
class TestServerPerformance(unittest.TestCase):
@classmethod
def setUpClass(cls):
"""Выполняется один раз для всего класса"""
cls.test_server = start_test_server()
@classmethod
def tearDownClass(cls):
"""Выполняется один раз после всех тестов класса"""
cls.test_server.stop()
def setUp(self):
"""Выполняется перед каждым тестом"""
self.start_time = time.time()
def tearDown(self):
"""Выполняется после каждого теста"""
duration = time.time() - self.start_time
if duration > 1.0: # Предупреждение о медленных тестах
print(f"Warning: {self._testMethodName} took {duration:.2f}s")
Интеграция с системами мониторинга
Для серверных сред полезно интегрировать результаты тестов с системами мониторинга:
# monitoring_integration.py
import unittest
import requests
import os
class MonitoringTestResult(unittest.TestResult):
def __init__(self):
super().__init__()
self.webhook_url = os.getenv('SLACK_WEBHOOK_URL')
def addError(self, test, err):
super().addError(test, err)
self.notify_error(test, err)
def addFailure(self, test, err):
super().addFailure(test, err)
self.notify_failure(test, err)
def notify_error(self, test, err):
if self.webhook_url:
message = f"🔴 Test ERROR: {test._testMethodName}\n```{err[1]}```"
self.send_notification(message)
def notify_failure(self, test, err):
if self.webhook_url:
message = f"🟡 Test FAILURE: {test._testMethodName}\n```{err[1]}```"
self.send_notification(message)
def send_notification(self, message):
try:
requests.post(self.webhook_url, json={"text": message})
except:
pass # Не ломаем тесты из-за проблем с уведомлениями
Нестандартные способы использования
Unittest можно использовать не только для тестирования кода, но и для:
Мониторинг инфраструктуры
class TestInfrastructure(unittest.TestCase):
def test_disk_space(self):
"""Проверяем свободное место на диске"""
import shutil
free_bytes = shutil.disk_usage('/').free
free_gb = free_bytes / (1024**3)
self.assertGreater(free_gb, 10, "Мало свободного места на диске")
def test_memory_usage(self):
"""Проверяем использование памяти"""
import psutil
memory = psutil.virtual_memory()
self.assertLess(memory.percent, 90, "Высокое использование памяти")
def test_critical_services(self):
"""Проверяем критические сервисы"""
critical_services = ['nginx', 'postgresql', 'redis']
for service in critical_services:
with self.subTest(service=service):
result = os.system(f'systemctl is-active {service}')
self.assertEqual(result, 0, f"Сервис {service} не запущен")
Валидация конфигурации
class TestConfiguration(unittest.TestCase):
def setUp(self):
with open('/etc/myapp/config.json') as f:
self.config = json.load(f)
def test_required_settings(self):
"""Проверяем обязательные настройки"""
required = ['database_url', 'secret_key', 'allowed_hosts']
for setting in required:
with self.subTest(setting=setting):
self.assertIn(setting, self.config)
self.assertNotEqual(self.config[setting], "")
def test_security_settings(self):
"""Проверяем настройки безопасности"""
self.assertFalse(self.config.get('debug', False))
self.assertTrue(self.config.get('use_https', False))
self.assertGreater(len(self.config.get('secret_key', '')), 32)
Развёртывание и хостинг
Для запуска тестов на production-серверах нужна правильная инфраструктура. Если ты ещё не определился с хостингом, рекомендую посмотреть на VPS решения для разработки и тестирования, или выделенные серверы для больших проектов с высокой нагрузкой.
Для автоматизации тестов на сервере создай systemd service:
# /etc/systemd/system/app-tests.service
[Unit]
Description=Application Tests
After=network.target
[Service]
Type=oneshot
User=www-data
WorkingDirectory=/var/www/myapp
ExecStart=/usr/bin/python -m unittest discover tests -v
Environment=PYTHONPATH=/var/www/myapp
[Install]
WantedBy=multi-user.target
И timer для регулярного запуска:
# /etc/systemd/system/app-tests.timer
[Unit]
Description=Run Application Tests
Requires=app-tests.service
[Timer]
OnCalendar=*:0/30 # Каждые 30 минут
Persistent=true
[Install]
WantedBy=timers.target
Полезные ссылки и ресурсы
Для углублённого изучения рекомендую:
Заключение и рекомендации
Unittest — это мощный инструмент, который должен быть в арсенале каждого, кто работает с серверным Python-кодом. Да, он может показаться многословным по сравнению с pytest, но его стандартность и богатый функционал делают его отличным выбором для корпоративных проектов и долгосрочной поддержки.
Основные рекомендации:
- Начни с простого — не пытайся покрыть тестами весь код сразу
- Тестируй критический функционал — аутентификация, обработка данных, API
- Используй моки для внешних зависимостей — не тестируй чужой код
- Автоматизируй запуск тестов — в CI/CD, pre-commit hooks, cron
- Следи за производительностью — медленные тесты никто не будет запускать
- Интегрируй с мониторингом — падающие тесты должны быть видны
Unittest особенно эффективен для серверных задач: тестирования API, валидации конфигурации, проверки инфраструктуры и мониторинга сервисов. Правильно настроенные тесты сэкономят тебе часы отладки и помогут спать спокойно, зная, что твой код работает как надо.
Помни: тесты — это не overhead, это инвестиция в стабильность и скорость разработки. Каждый найденный в тестах баг — это потенциальный аутейдж, которого удалось избежать.
В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.
Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.