UnitTest – встроенный тестировщик Python

онлайн тренажер по питону
Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в тестирование с unittest

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

UnitTest представляет собой встроенную библиотеку Python, основанную на архитектуре xUnit, которая предоставляет разработчикам полный набор инструментов для создания модульных тестов, проверки бизнес-логики и контроля работы функций и классов. Главное преимущество unittest заключается в том, что он является частью стандартной библиотеки Python и не требует дополнительных установок.

Что такое unittest и зачем он нужен

Unittest - это фреймворк для написания и выполнения тестов в Python, который следует принципам test-driven development (TDD). Он позволяет создавать повторяемые, изолированные и автоматизированные тесты, которые помогают:

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

Основы работы с unittest

Базовая структура теста

Для начала работы с unittest необходимо импортировать модуль и создать тестовый класс:

import unittest

class TestMath(unittest.TestCase):
    def test_add(self):
        self.assertEqual(2 + 3, 5)
        
    def test_multiply(self):
        self.assertEqual(4 * 5, 20)

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

Ключевые принципы структуры

Каждый тестовый класс должен наследоваться от unittest.TestCase. Все тестовые методы начинаются с префикса test_, что позволяет фреймворку автоматически обнаруживать и выполнять тесты. Метод unittest.main() запускает все найденные тесты в текущем модуле.

Жизненный цикл тестов и фикстуры

Методы установки и очистки

Unittest предоставляет специальные методы для подготовки и очистки тестового окружения:

class TestDatabase(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        """Выполняется один раз перед всеми тестами класса"""
        cls.db_connection = create_test_database()
    
    @classmethod
    def tearDownClass(cls):
        """Выполняется один раз после всех тестов класса"""
        cls.db_connection.close()
    
    def setUp(self):
        """Выполняется перед каждым тестом"""
        self.test_data = ["item1", "item2", "item3"]
    
    def tearDown(self):
        """Выполняется после каждого теста"""
        self.test_data.clear()

Уровни фикстур

Фикстуры в unittest работают на разных уровнях:

  • setUp() и tearDown() - для каждого теста индивидуально
  • setUpClass() и tearDownClass() - для всего тестового класса
  • setUpModule() и tearDownModule() - для всего модуля

Методы утверждений (Assertions)

Основные методы проверки

Unittest предоставляет богатый набор методов для различных типов проверок:

Метод Описание Пример использования
assertEqual(a, b) Проверяет равенство a и b self.assertEqual(2+2, 4)
assertNotEqual(a, b) Проверяет неравенство a и b self.assertNotEqual(2+2, 5)
assertTrue(x) Проверяет истинность x self.assertTrue(5 > 3)
assertFalse(x) Проверяет ложность x self.assertFalse(5 < 3)
assertIs(a, b) Проверяет идентичность объектов self.assertIs(x, None)
assertIsNot(a, b) Проверяет различность объектов self.assertIsNot(x, None)
assertIsNone(x) Проверяет, что x равно None self.assertIsNone(result)
assertIsNotNone(x) Проверяет, что x не равно None self.assertIsNotNone(result)
assertIn(a, b) Проверяет наличие a в b self.assertIn('test', text)
assertNotIn(a, b) Проверяет отсутствие a в b self.assertNotIn('bug', text)
assertIsInstance(a, b) Проверяет тип объекта self.assertIsInstance(result, list)
assertNotIsInstance(a, b) Проверяет несоответствие типа self.assertNotIsInstance(result, str)

Специализированные методы

Метод Описание Пример использования
assertAlmostEqual(a, b) Проверяет приблизительное равенство self.assertAlmostEqual(3.14159, 3.14, places=2)
assertGreater(a, b) Проверяет, что a > b self.assertGreater(10, 5)
assertLess(a, b) Проверяет, что a < b self.assertLess(5, 10)
assertGreaterEqual(a, b) Проверяет, что a >= b self.assertGreaterEqual(10, 10)
assertLessEqual(a, b) Проверяет, что a <= b self.assertLessEqual(5, 10)
assertRegex(text, regex) Проверяет соответствие регулярному выражению self.assertRegex(email, r'.*@.*\..*')
assertCountEqual(a, b) Проверяет равенство последовательностей self.assertCountEqual([1,2,3], [3,2,1])

Тестирование исключений

Основные подходы

Unittest предоставляет несколько способов проверки исключений:

class TestExceptions(unittest.TestCase):
    def test_division_by_zero(self):
        with self.assertRaises(ZeroDivisionError):
            result = 10 / 0
    
    def test_value_error_with_message(self):
        with self.assertRaises(ValueError) as context:
            int("not_a_number")
        self.assertIn("invalid literal", str(context.exception))
    
    def test_custom_exception(self):
        with self.assertRaises(CustomException):
            raise_custom_exception()

Проверка сообщений исключений

def test_exception_message(self):
    with self.assertRaisesRegex(ValueError, "must be positive"):
        validate_positive_number(-5)

Работа с Mock-объектами

Основы мокирования

Модуль unittest.mock позволяет заменять реальные объекты и функции на контролируемые заглушки:

from unittest.mock import Mock, patch, MagicMock

class TestWithMocks(unittest.TestCase):
    def test_function_with_mock(self):
        mock_obj = Mock()
        mock_obj.method.return_value = 42
        
        result = mock_obj.method()
        self.assertEqual(result, 42)
        mock_obj.method.assert_called_once()

Патчинг функций и методов

class TestPatching(unittest.TestCase):
    @patch('requests.get')
    def test_api_call(self, mock_get):
        mock_get.return_value.status_code = 200
        mock_get.return_value.json.return_value = {'data': 'test'}
        
        response = make_api_call()
        self.assertEqual(response['data'], 'test')
    
    def test_with_context_manager(self):
        with patch('os.path.exists') as mock_exists:
            mock_exists.return_value = True
            result = check_file_exists('/path/to/file')
            self.assertTrue(result)

Способы запуска тестов

Из командной строки

# Запуск всех тестов в текущей директории
python -m unittest discover

# Запуск конкретного модуля
python -m unittest test_module

# Запуск конкретного класса
python -m unittest test_module.TestClass

# Запуск конкретного метода
python -m unittest test_module.TestClass.test_method

# Запуск с подробным выводом
python -m unittest -v test_module

Программный запуск

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

Создание тестовых наборов

def create_test_suite():
    suite = unittest.TestSuite()
    suite.addTest(TestMath('test_add'))
    suite.addTest(TestMath('test_multiply'))
    return suite

runner = unittest.TextTestRunner(verbosity=2)
result = runner.run(create_test_suite())

Параметризация тестов

Использование subTest

class TestParametrized(unittest.TestCase):
    def test_multiple_values(self):
        test_cases = [
            (2, 3, 5),
            (1, 4, 5),
            (0, 0, 0),
            (-1, 1, 0)
        ]
        
        for a, b, expected in test_cases:
            with self.subTest(a=a, b=b):
                result = add(a, b)
                self.assertEqual(result, expected)

Создание параметризованных тестов

def create_parametrized_test(a, b, expected):
    def test_method(self):
        self.assertEqual(add(a, b), expected)
    return test_method

# Динамическое создание тестов
test_cases = [(2, 3, 5), (1, 4, 5), (0, 0, 0)]
for i, (a, b, expected) in enumerate(test_cases):
    test_method = create_parametrized_test(a, b, expected)
    setattr(TestMath, f'test_add_{i}', test_method)

Пропуск и условное выполнение тестов

Декораторы для управления тестами

import sys

class TestConditional(unittest.TestCase):
    @unittest.skip("Временно отключен")
    def test_skip_always(self):
        pass
    
    @unittest.skipIf(sys.version_info < (3, 8), "Требует Python 3.8+")
    def test_skip_if_condition(self):
        pass
    
    @unittest.skipUnless(sys.platform.startswith("win"), "Только для Windows")
    def test_skip_unless_condition(self):
        pass
    
    @unittest.expectedFailure
    def test_expected_failure(self):
        self.assertEqual(1, 2)  # Заведомо неверное утверждение

Интеграция с IDE и CI/CD

Настройка в популярных IDE

Unittest поддерживается всеми основными IDE:

  • PyCharm: встроенная поддержка с графическим интерфейсом
  • VS Code: расширение Python Test Explorer
  • Sublime Text: пакет UnitTesting

Интеграция с GitHub Actions

name: Run Tests

on: [push, pull_request]

jobs:
  test:
    runs-on: ubuntu-latest
    strategy:
      matrix:
        python-version: [3.8, 3.9, '3.10', '3.11']

    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 -s tests -p "test_*.py" -v

Практические примеры

Тестирование веб-API

import unittest
from unittest.mock import patch
import requests

class TestAPIClient(unittest.TestCase):
    def setUp(self):
        self.base_url = "https://api.example.com"
        self.client = APIClient(self.base_url)
    
    @patch('requests.get')
    def test_get_user_success(self, mock_get):
        mock_response = Mock()
        mock_response.status_code = 200
        mock_response.json.return_value = {'id': 1, 'name': 'Test User'}
        mock_get.return_value = mock_response
        
        user = self.client.get_user(1)
        
        self.assertEqual(user['id'], 1)
        self.assertEqual(user['name'], 'Test User')
        mock_get.assert_called_once_with(f"{self.base_url}/users/1")
    
    @patch('requests.get')
    def test_get_user_not_found(self, mock_get):
        mock_response = Mock()
        mock_response.status_code = 404
        mock_get.return_value = mock_response
        
        with self.assertRaises(UserNotFoundError):
            self.client.get_user(999)

Тестирование файловых операций

import unittest
import tempfile
import os
from unittest.mock import patch

class TestFileOperations(unittest.TestCase):
    def setUp(self):
        self.temp_dir = tempfile.mkdtemp()
        self.test_file = os.path.join(self.temp_dir, 'test.txt')
    
    def tearDown(self):
        if os.path.exists(self.test_file):
            os.remove(self.test_file)
        os.rmdir(self.temp_dir)
    
    def test_write_read_file(self):
        content = "Test content"
        write_to_file(self.test_file, content)
        
        self.assertTrue(os.path.exists(self.test_file))
        read_content = read_from_file(self.test_file)
        self.assertEqual(content, read_content)
    
    @patch('builtins.open', side_effect=IOError("Permission denied"))
    def test_file_permission_error(self, mock_open):
        with self.assertRaises(IOError):
            write_to_file(self.test_file, "content")

Лучшие практики

Именование тестов

class TestUserValidator(unittest.TestCase):
    def test_valid_email_passes_validation(self):
        # Описательные имена тестов
        pass
    
    def test_invalid_email_raises_validation_error(self):
        pass
    
    def test_empty_email_raises_validation_error(self):
        pass

Организация тестового кода

class TestCalculator(unittest.TestCase):
    def setUp(self):
        self.calculator = Calculator()
    
    def test_add_positive_numbers(self):
        result = self.calculator.add(2, 3)
        self.assertEqual(result, 5)
    
    def test_add_negative_numbers(self):
        result = self.calculator.add(-2, -3)
        self.assertEqual(result, -5)
    
    def test_add_zero(self):
        result = self.calculator.add(5, 0)
        self.assertEqual(result, 5)

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

Unittest vs Pytest

Характеристика unittest pytest
Встроенность Входит в стандартную библиотеку Требует установки
Синтаксис Более формальный, основан на классах Простые функции
Фикстуры setUp/tearDown методы Декораторы @pytest.fixture
Параметризация Через subTest Встроенная @pytest.mark.parametrize
Плагины Ограниченная экосистема Богатая экосистема плагинов
Отчеты Базовые текстовые отчеты Подробные отчеты, HTML-отчеты

Unittest vs Nose2

Характеристика unittest nose2
Обнаружение тестов Базовое Расширенное
Конфигурация Программная Файлы конфигурации
Плагины Ограниченные Система плагинов
Производительность Стандартная Оптимизированная

Отладка и диагностика

Использование отладочной информации

class TestDebug(unittest.TestCase):
    def test_with_debug_info(self):
        expected = 10
        actual = complex_calculation()
        
        self.assertEqual(
            actual, 
            expected, 
            f"Expected {expected}, but got {actual}. "
            f"Input parameters: {self.input_params}"
        )
    
    def test_with_detailed_assertions(self):
        result = process_data(self.test_data)
        
        # Проверяем различные аспекты результата
        self.assertIsInstance(result, dict)
        self.assertIn('status', result)
        self.assertEqual(result['status'], 'success')
        self.assertGreater(len(result['data']), 0)

Кастомные методы утверждений

class ExtendedTestCase(unittest.TestCase):
    def assertBetween(self, value, min_val, max_val):
        """Проверяет, что значение находится в заданном диапазоне"""
        if not (min_val <= value <= max_val):
            raise AssertionError(f"{value} is not between {min_val} and {max_val}")
    
    def assertListAlmostEqual(self, list1, list2, places=7):
        """Проверяет приблизительное равенство списков чисел"""
        self.assertEqual(len(list1), len(list2))
        for a, b in zip(list1, list2):
            self.assertAlmostEqual(a, b, places=places)

Производительность и оптимизация

Ускорение выполнения тестов

class TestPerformance(unittest.TestCase):
    @classmethod
    def setUpClass(cls):
        # Создание дорогостоящих ресурсов один раз
        cls.expensive_resource = create_expensive_resource()
    
    def test_fast_operation(self):
        # Используем общий ресурс
        result = self.expensive_resource.quick_operation()
        self.assertIsNotNone(result)
    
    @unittest.skipIf(os.environ.get('SKIP_SLOW_TESTS'), "Пропуск медленных тестов")
    def test_slow_operation(self):
        # Медленный тест, который можно пропустить
        result = time_consuming_operation()
        self.assertTrue(result)

Параллельное выполнение

# Использование unittest-parallel для параллельного выполнения
# pip install unittest-parallel

# Запуск: python -m unittest_parallel

Часто задаваемые вопросы

Что такое unittest и почему он важен?

Unittest - это встроенный в Python фреймворк для автоматизированного тестирования, который помогает обеспечить качество и надежность кода путем создания повторяемых и изолированных тестов.

Как unittest отличается от pytest?

Unittest требует создания классов, наследующихся от TestCase, и использует более формальный синтаксис, в то время как pytest позволяет писать тесты как простые функции. Unittest входит в стандартную библиотеку, а pytest требует отдельной установки.

Можно ли использовать unittest для тестирования асинхронного кода?

Да, начиная с Python 3.8 unittest поддерживает асинхронные тесты через класс unittest.IsolatedAsyncioTestCase. Для более ранних версий необходимо использовать дополнительные библиотеки.

Как правильно организовать структуру тестов?

Рекомендуется создавать отдельную директорию tests с модулями, соответствующими структуре основного кода. Каждый тестовый файл должен начинаться с test_ и содержать классы, наследующиеся от unittest.TestCase.

Поддерживает ли unittest mock-объекты?

Да, unittest включает модуль unittest.mock с полным набором инструментов для создания заглушек, патчинга функций и контроля взаимодействий между объектами.

Как запускать только определенные тесты?

Можно использовать командную строку с указанием конкретного модуля, класса или метода: python -m unittest test_module.TestClass.test_method. Также доступны декораторы для пропуска тестов.

Можно ли параметризовать тесты в unittest?

Unittest не имеет встроенной параметризации как pytest, но можно использовать subTest() для создания подтестов или динамически генерировать тестовые методы.

Как интегрировать unittest с системами непрерывной интеграции?

Unittest легко интегрируется с CI/CD системами через командную строку. Большинство CI систем (GitHub Actions, GitLab CI, Jenkins) имеют встроенную поддержку unittest.

Можно ли создавать собственные методы утверждений?

Да, можно создавать кастомные методы утверждений, наследуясь от unittest.TestCase и добавляя собственные методы проверки.

Как тестировать исключения в unittest?

Используйте контекстный менеджер with self.assertRaises(ExceptionType): для проверки возникновения исключений или assertRaisesRegex() для проверки сообщений исключений.

Заключение

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

Несмотря на то, что более современные фреймворки, такие как pytest, предлагают более лаконичный синтаксис и дополнительные возможности, unittest продолжает оставаться отличным выбором для проектов, где важна простота развертывания и отсутствие внешних зависимостей.

Освоение unittest дает разработчикам прочную основу для понимания принципов тестирования и может служить отправной точкой для изучения более продвинутых тестовых фреймворков. Регулярное использование unittest в разработке помогает создавать более качественный, надежный и поддерживаемый код.

Новости