Home » Как использовать unittest для написания тестов в Python
Как использовать unittest для написания тестов в Python

Как использовать unittest для написания тестов в Python

Тесты — это не просто красивая надпись в коде, которую все видят, но никто не читает. Это ваш главный спасательный круг в мире серверного развития, где один неправильный коммит может положить продакшн на лопатки. Если вы настраиваете сервера, пишете скрипты для автоматизации или просто хотите спать спокойно, зная, что ваш код не взорвётся в самый неподходящий момент — unittest в Python станет вашим лучшим другом.

Сегодня разберём, как правильно использовать unittest для тестирования Python-скриптов. Покажу реальные примеры из серверной практики, дам готовые команды и расскажу, как интегрировать тесты в ваш рабочий процесс. Особенно актуально для тех, кто работает с конфигурационными скриптами, мониторингом серверов и автоматизацией деплоя.

Как работает unittest — базовые концепции

unittest — это встроенный фреймворк для тестирования в Python, который не требует дополнительных установок. Он работает по принципу xUnit (как JUnit для Java), что означает знакомую структуру для многих разработчиков.

Основные компоненты unittest:

  • TestCase — базовый класс для всех тестов
  • TestSuite — группа тестов
  • TestRunner — выполняет тесты и показывает результаты
  • Assert методы — проверяют условия

Классический пример теста выглядит так:

import unittest

class TestServerHealth(unittest.TestCase):
    def test_server_response(self):
        # Тестируем, что сервер отвечает
        response = ping_server('192.168.1.100')
        self.assertTrue(response)
    
    def test_port_availability(self):
        # Проверяем доступность порта
        port_status = check_port('192.168.1.100', 80)
        self.assertEqual(port_status, 'open')

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

Пошаговая настройка unittest для серверных задач

Начнём с создания простой структуры проекта для тестирования серверных скриптов:

# Создаём структуру проекта
mkdir server_monitoring
cd server_monitoring
mkdir tests
touch server_utils.py
touch tests/test_server_utils.py
touch tests/__init__.py

Теперь напишем базовый модуль для работы с серверами:

# server_utils.py
import socket
import subprocess
import os

def ping_host(hostname, timeout=5):
    """Проверяет доступность хоста"""
    try:
        result = subprocess.run(['ping', '-c', '1', '-W', str(timeout), hostname], 
                              capture_output=True, text=True)
        return result.returncode == 0
    except Exception:
        return False

def check_port(hostname, port, timeout=5):
    """Проверяет доступность порта"""
    try:
        with socket.create_connection((hostname, port), timeout):
            return True
    except (socket.timeout, socket.error):
        return False

def get_disk_usage(path='/'):
    """Получает информацию об использовании диска"""
    try:
        stat = os.statvfs(path)
        total = stat.f_blocks * stat.f_frsize
        free = stat.f_bavail * stat.f_frsize
        used = total - free
        return {
            'total': total,
            'used': used,
            'free': free,
            'percentage': round((used / total) * 100, 2)
        }
    except Exception:
        return None

А теперь создадим тесты для этих функций:

# tests/test_server_utils.py
import unittest
from unittest.mock import patch, MagicMock
import sys
import os

# Добавляем путь к модулю
sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from server_utils import ping_host, check_port, get_disk_usage

class TestServerUtils(unittest.TestCase):
    
    def setUp(self):
        """Выполняется перед каждым тестом"""
        self.test_hostname = 'example.com'
        self.test_port = 80
    
    def tearDown(self):
        """Выполняется после каждого теста"""
        pass
    
    @patch('subprocess.run')
    def test_ping_host_success(self, mock_run):
        """Тест успешного пинга"""
        mock_run.return_value.returncode = 0
        result = ping_host(self.test_hostname)
        self.assertTrue(result)
        mock_run.assert_called_once()
    
    @patch('subprocess.run')
    def test_ping_host_failure(self, mock_run):
        """Тест неуспешного пинга"""
        mock_run.return_value.returncode = 1
        result = ping_host(self.test_hostname)
        self.assertFalse(result)
    
    @patch('socket.create_connection')
    def test_check_port_open(self, mock_socket):
        """Тест открытого порта"""
        mock_socket.return_value.__enter__.return_value = MagicMock()
        result = check_port(self.test_hostname, self.test_port)
        self.assertTrue(result)
    
    @patch('socket.create_connection')
    def test_check_port_closed(self, mock_socket):
        """Тест закрытого порта"""
        mock_socket.side_effect = socket.timeout()
        result = check_port(self.test_hostname, self.test_port)
        self.assertFalse(result)
    
    @patch('os.statvfs')
    def test_get_disk_usage(self, mock_statvfs):
        """Тест получения информации о диске"""
        mock_stat = MagicMock()
        mock_stat.f_blocks = 1000
        mock_stat.f_frsize = 4096
        mock_stat.f_bavail = 500
        mock_statvfs.return_value = mock_stat
        
        result = get_disk_usage()
        self.assertIsNotNone(result)
        self.assertEqual(result['total'], 1000 * 4096)
        self.assertEqual(result['free'], 500 * 4096)

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

Запуск тестов и интерпретация результатов

Существует несколько способов запуска тестов unittest:

# Запуск одного файла с тестами
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_ping_host_success

Пример вывода успешного теста:

test_check_port_closed (tests.test_server_utils.TestServerUtils) ... ok
test_check_port_open (tests.test_server_utils.TestServerUtils) ... ok
test_get_disk_usage (tests.test_server_utils.TestServerUtils) ... ok
test_ping_host_failure (tests.test_server_utils.TestServerUtils) ... ok
test_ping_host_success (tests.test_server_utils.TestServerUtils) ... ok

----------------------------------------------------------------------
Ran 5 tests in 0.003s

OK

Практические кейсы и примеры использования

Рассмотрим реальные сценарии использования unittest для серверных задач:

Тестирование скриптов мониторинга

# monitoring.py
import psutil
import requests

class ServerMonitor:
    def __init__(self, server_url):
        self.server_url = server_url
    
    def check_cpu_usage(self):
        return psutil.cpu_percent(interval=1)
    
    def check_memory_usage(self):
        memory = psutil.virtual_memory()
        return memory.percent
    
    def check_http_status(self):
        try:
            response = requests.get(self.server_url, timeout=10)
            return response.status_code
        except requests.exceptions.RequestException:
            return None

# tests/test_monitoring.py
import unittest
from unittest.mock import patch, MagicMock
import sys
import os

sys.path.insert(0, os.path.dirname(os.path.dirname(os.path.abspath(__file__))))
from monitoring import ServerMonitor

class TestServerMonitor(unittest.TestCase):
    
    def setUp(self):
        self.monitor = ServerMonitor('http://example.com')
    
    @patch('psutil.cpu_percent')
    def test_cpu_usage_normal(self, mock_cpu):
        """Тест нормального использования CPU"""
        mock_cpu.return_value = 25.5
        result = self.monitor.check_cpu_usage()
        self.assertEqual(result, 25.5)
        self.assertLess(result, 80)  # Проверяем, что CPU не перегружен
    
    @patch('psutil.virtual_memory')
    def test_memory_usage_critical(self, mock_memory):
        """Тест критического использования памяти"""
        mock_memory.return_value.percent = 95.0
        result = self.monitor.check_memory_usage()
        self.assertEqual(result, 95.0)
        self.assertGreater(result, 90)  # Критический уровень
    
    @patch('requests.get')
    def test_http_status_ok(self, mock_get):
        """Тест успешного HTTP-запроса"""
        mock_response = MagicMock()
        mock_response.status_code = 200
        mock_get.return_value = mock_response
        
        result = self.monitor.check_http_status()
        self.assertEqual(result, 200)
    
    @patch('requests.get')
    def test_http_status_timeout(self, mock_get):
        """Тест таймаута HTTP-запроса"""
        mock_get.side_effect = requests.exceptions.Timeout()
        result = self.monitor.check_http_status()
        self.assertIsNone(result)

Тестирование конфигурационных скриптов

# config_manager.py
import json
import os

class ConfigManager:
    def __init__(self, config_path):
        self.config_path = config_path
        self.config = {}
    
    def load_config(self):
        try:
            with open(self.config_path, 'r') as f:
                self.config = json.load(f)
            return True
        except (FileNotFoundError, json.JSONDecodeError):
            return False
    
    def get_setting(self, key, default=None):
        return self.config.get(key, default)
    
    def validate_config(self):
        required_keys = ['server_host', 'server_port', 'database_url']
        for key in required_keys:
            if key not in self.config:
                return False, f"Missing required key: {key}"
        return True, "Config is valid"

# tests/test_config_manager.py
import unittest
import tempfile
import json
import os
from config_manager import ConfigManager

class TestConfigManager(unittest.TestCase):
    
    def setUp(self):
        # Создаём временный файл конфигурации
        self.temp_config = tempfile.NamedTemporaryFile(mode='w', delete=False, suffix='.json')
        self.config_data = {
            'server_host': '192.168.1.100',
            'server_port': 8080,
            'database_url': 'postgresql://user:pass@localhost/db'
        }
        json.dump(self.config_data, self.temp_config)
        self.temp_config.close()
        
        self.config_manager = ConfigManager(self.temp_config.name)
    
    def tearDown(self):
        # Удаляем временный файл
        os.unlink(self.temp_config.name)
    
    def test_load_config_success(self):
        """Тест успешной загрузки конфигурации"""
        result = self.config_manager.load_config()
        self.assertTrue(result)
        self.assertEqual(self.config_manager.config['server_host'], '192.168.1.100')
    
    def test_load_config_file_not_found(self):
        """Тест загрузки несуществующего файла"""
        manager = ConfigManager('/nonexistent/path.json')
        result = manager.load_config()
        self.assertFalse(result)
    
    def test_get_setting_existing(self):
        """Тест получения существующего параметра"""
        self.config_manager.load_config()
        result = self.config_manager.get_setting('server_port')
        self.assertEqual(result, 8080)
    
    def test_get_setting_with_default(self):
        """Тест получения несуществующего параметра с дефолтом"""
        self.config_manager.load_config()
        result = self.config_manager.get_setting('nonexistent', 'default_value')
        self.assertEqual(result, 'default_value')
    
    def test_validate_config_success(self):
        """Тест успешной валидации конфигурации"""
        self.config_manager.load_config()
        is_valid, message = self.config_manager.validate_config()
        self.assertTrue(is_valid)
        self.assertEqual(message, "Config is valid")
    
    def test_validate_config_missing_key(self):
        """Тест валидации с отсутствующим ключом"""
        incomplete_config = {'server_host': '192.168.1.100'}
        self.config_manager.config = incomplete_config
        
        is_valid, message = self.config_manager.validate_config()
        self.assertFalse(is_valid)
        self.assertIn("Missing required key", message)

Сравнение unittest с другими фреймворками

Давайте сравним unittest с популярными альтернативами:

Фреймворк Преимущества Недостатки Лучше использовать для
unittest Встроенный, стандартный подход, хорошая документация Многословный синтаксис, ограниченные возможности Простых проектов, стандартного тестирования
pytest Простой синтаксис, мощные фикстуры, плагины Дополнительная зависимость, может быть избыточным Сложных проектов, когда нужна гибкость
nose2 Совместим с unittest, расширенные возможности Менее популярен, ограниченное сообщество Миграции с unittest, когда нужны дополнительные фичи

Продвинутые возможности unittest

Использование setUp и tearDown для работы с серверами

import unittest
import subprocess
import time

class TestServerDeployment(unittest.TestCase):
    
    @classmethod
    def setUpClass(cls):
        """Выполняется один раз для всего класса"""
        print("Запускаем тестовый сервер...")
        cls.server_process = subprocess.Popen(['python', '-m', 'http.server', '8000'])
        time.sleep(2)  # Даём серверу время на запуск
    
    @classmethod
    def tearDownClass(cls):
        """Выполняется один раз после всех тестов"""
        print("Останавливаем тестовый сервер...")
        cls.server_process.terminate()
        cls.server_process.wait()
    
    def setUp(self):
        """Выполняется перед каждым тестом"""
        self.base_url = 'http://localhost:8000'
    
    def test_server_responds(self):
        """Тест отклика сервера"""
        import requests
        response = requests.get(self.base_url)
        self.assertEqual(response.status_code, 200)
    
    def test_server_headers(self):
        """Тест заголовков сервера"""
        import requests
        response = requests.get(self.base_url)
        self.assertIn('Server', response.headers)

Тестирование с использованием subTest

class TestMultipleServers(unittest.TestCase):
    
    def test_multiple_servers_health(self):
        """Тест состояния нескольких серверов"""
        servers = [
            ('web1.example.com', 80),
            ('web2.example.com', 80),
            ('db.example.com', 5432),
            ('cache.example.com', 6379)
        ]
        
        for host, port in servers:
            with self.subTest(host=host, port=port):
                # Тест будет продолжен даже если один из серверов недоступен
                result = check_port(host, port)
                self.assertTrue(result, f"Server {host}:{port} is not responding")

Интеграция с CI/CD и автоматизацией

Для интеграции unittest в процесс автоматизации создайте скрипт запуска тестов:

# run_tests.sh
#!/bin/bash

echo "Запуск тестов серверных компонентов..."

# Запускаем тесты с подробным выводом
python -m unittest discover -s tests/ -v

# Проверяем код возврата
if [ $? -eq 0 ]; then
    echo "✅ Все тесты прошли успешно!"
    exit 0
else
    echo "❌ Некоторые тесты провалились!"
    exit 1
fi

Для GitHub Actions создайте файл `.github/workflows/tests.yml`:

name: Server 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 -r requirements.txt
    - name: Run tests
      run: |
        python -m unittest discover -s tests/ -v

Генерация отчётов о покрытии кода

Для анализа покрытия кода тестами используйте coverage.py:

# Установка
pip install coverage

# Запуск тестов с измерением покрытия
coverage run -m unittest discover tests/

# Генерация отчёта
coverage report -m

# Генерация HTML-отчёта
coverage html

Нестандартные способы использования

Тестирование производительности

import unittest
import time

class TestPerformance(unittest.TestCase):
    
    def test_function_performance(self):
        """Тест производительности функции"""
        start_time = time.time()
        
        # Выполняем тестируемую функцию
        result = some_heavy_function()
        
        end_time = time.time()
        execution_time = end_time - start_time
        
        # Проверяем, что функция выполнилась быстрее 5 секунд
        self.assertLess(execution_time, 5.0, 
                       f"Function took {execution_time:.2f} seconds, expected < 5.0")

Тестирование с использованием Docker

import unittest
import docker

class TestDockerContainer(unittest.TestCase):
    
    def setUp(self):
        self.client = docker.from_env()
        self.container = self.client.containers.run(
            "nginx:alpine",
            ports={'80/tcp': 8080},
            detach=True
        )
        time.sleep(5)  # Ждём запуска контейнера
    
    def tearDown(self):
        self.container.stop()
        self.container.remove()
    
    def test_container_responds(self):
        """Тест отклика контейнера"""
        import requests
        response = requests.get('http://localhost:8080')
        self.assertEqual(response.status_code, 200)

Автоматизация серверных задач с unittest

unittest открывает широкие возможности для автоматизации:

  • Тестирование деплоя — проверка корректности развёртывания приложений
  • Мониторинг инфраструктуры — регулярные проверки состояния серверов
  • Валидация конфигураций — проверка настроек перед применением
  • Интеграционное тестирование — проверка взаимодействия сервисов
  • Нагрузочное тестирование — проверка производительности системы

Пример скрипта для автоматической проверки инфраструктуры:

# infrastructure_tests.py
import unittest
import json
import sys

class InfrastructureHealthCheck(unittest.TestCase):
    
    def setUp(self):
        with open('infrastructure.json', 'r') as f:
            self.infrastructure = json.load(f)
    
    def test_all_servers_reachable(self):
        """Проверка доступности всех серверов"""
        for server in self.infrastructure['servers']:
            with self.subTest(server=server['name']):
                result = ping_host(server['host'])
                self.assertTrue(result, f"Server {server['name']} is unreachable")
    
    def test_all_services_running(self):
        """Проверка работы всех сервисов"""
        for service in self.infrastructure['services']:
            with self.subTest(service=service['name']):
                result = check_port(service['host'], service['port'])
                self.assertTrue(result, f"Service {service['name']} is not responding")

# Запуск с выводом результатов
if __name__ == '__main__':
    # Создаём кастомный runner для красивого вывода
    runner = unittest.TextTestRunner(verbosity=2)
    result = runner.run(unittest.TestLoader().loadTestsFromTestCase(InfrastructureHealthCheck))
    
    if not result.wasSuccessful():
        sys.exit(1)

Для серверных задач особенно полезно настроить VPS или выделенный сервер специально для запуска тестов — это позволит изолировать тестовую среду от продакшена.

Заключение и рекомендации

unittest — это мощный и надёжный инструмент для тестирования Python-кода, который особенно хорош для серверных задач. Он не требует дополнительных зависимостей, хорошо документирован и интегрируется с большинством IDE и CI/CD систем.

Когда использовать unittest:

  • Для простых и средних проектов
  • Когда нужна стабильность и предсказуемость
  • В корпоративной среде, где важна стандартизация
  • Для тестирования серверных скриптов и утилит

Когда стоит рассмотреть альтернативы:

  • В больших проектах с комплексными тестами (pytest)
  • Когда нужны продвинутые фикстуры и плагины
  • Для BDD-подхода (behave, pytest-bdd)

Главное правило — начинайте писать тесты прямо сейчас, даже если они простые. Лучше иметь базовые тесты на unittest, чем вообще никаких тестов. Ваши серверы и нервная система скажут вам спасибо, когда в 3 часа ночи тесты поймают критическую ошибку до того, как она попадёт в продакшн.

Удачного тестирования! 🚀


В этой статье собрана информация и материалы из различных интернет-источников. Мы признаем и ценим работу всех оригинальных авторов, издателей и веб-сайтов. Несмотря на то, что были приложены все усилия для надлежащего указания исходного материала, любая непреднамеренная оплошность или упущение не являются нарушением авторских прав. Все упомянутые товарные знаки, логотипы и изображения являются собственностью соответствующих владельцев. Если вы считаете, что какой-либо контент, использованный в этой статье, нарушает ваши авторские права, немедленно свяжитесь с нами для рассмотрения и принятия оперативных мер.

Данная статья предназначена исключительно для ознакомительных и образовательных целей и не ущемляет права правообладателей. Если какой-либо материал, защищенный авторским правом, был использован без должного упоминания или с нарушением законов об авторском праве, это непреднамеренно, и мы исправим это незамедлительно после уведомления. Обратите внимание, что переиздание, распространение или воспроизведение части или всего содержимого в любой форме запрещено без письменного разрешения автора и владельца веб-сайта. Для получения разрешений или дополнительных запросов, пожалуйста, свяжитесь с нами.

Leave a reply

Your email address will not be published. Required fields are marked