Как в Python писать тесты: руководство по unittest
Тестирование — это важный этап разработки программного обеспечения, который позволяет убедиться в корректности работы кода. В Python одним из популярных инструментов для написания модульных тестов является встроенная библиотека unittest.
В этом руководстве мы подробно рассмотрим, как использовать unittest для написания тестов, с примерами и объяснениями кода.
Что такое unittest
?
unittest
— это стандартная библиотека Python для написания и выполнения тестов. Она базируется на xUnit архитектуре для тестирования и является мощным инструментом, который помогает автоматизировать процесс проверки кода. Он включает в себя следующие возможности:
- Создание и организация тестов.
- Проверка ожиданий с помощью утверждений (assertions).
- Инициализация и очистка окружения перед тестами и после их выполнения.
- Группировка тестов в тестовые наборы (test suites).
- Генерация отчетов о результатах тестирования.
Установка и импорт
Так как unittest
является частью стандартной библиотеки Python, вам не нужно устанавливать его дополнительно. Вы можете просто импортировать его в коде:
import unittest
Базовый пример теста с использованием unittest
Давайте рассмотрим простой пример. Предположим, у нас есть функция, которая складывает два числа:
def add(a, b):
return a + b
Теперь мы создадим тест для этой функции с помощью unittest
:
import unittest
# Тестируемая функция
def add(a, b):
return a + b
# Создание класса для тестов
class TestMathFunctions(unittest.TestCase):
# Метод для тестирования функции add
def test_add(self):
self.assertEqual(add(2, 3), 5) # Проверяем, что 2 + 3 = 5
self.assertEqual(add(-1, 1), 0) # Проверяем, что -1 + 1 = 0
self.assertEqual(add(0, 0), 0) # Проверяем, что 0 + 0 = 0
# Запуск тестов
if __name__ == '__main__':
unittest.main()
В этом примере:
- Мы создали класс
TestMathFunctions
, который наследуетunittest.TestCase
. Это позволяет нам использовать все встроенные методы для тестирования. - Метод
test_add
содержит несколько проверок с использованием методаassertEqual
, который проверяет, равны ли два значения. unittest.main()
используется для запуска всех тестов в модуле.
Утверждения (assertions) в unittest
unittest
предлагает широкий набор методов утверждений, которые можно использовать для проверки корректности работы кода. Вот некоторые из них:
assertEqual(a, b)
— проверяет, чтоa == b
.assertNotEqual(a, b)
— проверяет, чтоa != b
.assertTrue(x)
— проверяет, чтоx
истинно.assertFalse(x)
— проверяет, чтоx
ложно.assertIsNone(x)
— проверяет, чтоx
равноNone
.assertIsNotNone(x)
— проверяет, чтоx
не равноNone
.assertRaises(Exception)
— проверяет, что вызывается исключение.
Пример использования различных утверждений:
import unittest
def divide(a, b):
if b == 0:
raise ValueError("Division by zero!")
return a / b
class TestDivision(unittest.TestCase):
def test_divide(self):
self.assertEqual(divide(10, 2), 5)
self.assertTrue(divide(9, 3) == 3)
self.assertRaises(ValueError, divide, 10, 0)
if __name__ == '__main__':
unittest.main()
В этом примере:
- Используем
assertEqual
для проверки результата функции деления. - Используем
assertTrue
для проверки истинности утверждения. - Используем
assertRaises
для проверки, что при делении на 0 возникает исключениеValueError
.
Инициализация и завершение тестов: setUp()
и tearDown()
Иногда необходимо выполнять определенные действия перед началом каждого теста и после его завершения. Например, вы можете захотеть создать временные данные или очистить состояние системы. Для этого используются методы setUp()
и tearDown()
.
Пример:
import unittest
class TestSetupTearDown(unittest.TestCase):
def setUp(self):
# Этот код выполняется перед каждым тестом
self.test_data = [1, 2, 3, 4]
def tearDown(self):
# Этот код выполняется после каждого теста
self.test_data = None
def test_sum(self):
self.assertEqual(sum(self.test_data), 10)
if __name__ == '__main__':
unittest.main()
Здесь:
setUp()
инициализирует списокtest_data
перед каждым тестом.tearDown()
очищает ресурсы после завершения каждого теста.
Тестовые наборы (Test Suites)
Если у вас много тестов, вы можете организовать их в тестовые наборы. Это позволяет группировать тесты и запускать их вместе.
Пример создания тестового набора:
import unittest
class TestMath(unittest.TestCase):
def test_add(self):
self.assertEqual(1 + 1, 2)
def test_subtract(self):
self.assertEqual(5 - 3, 2)
class TestStringMethods(unittest.TestCase):
def test_upper(self):
self.assertEqual('foo'.upper(), 'FOO')
def test_isupper(self):
self.assertTrue('FOO'.isupper())
# Создаем тестовый набор
def suite():
suite = unittest.TestSuite()
suite.addTest(TestMath('test_add'))
suite.addTest(TestMath('test_subtract'))
suite.addTest(TestStringMethods('test_upper'))
return suite
if __name__ == '__main__':
runner = unittest.TextTestRunner()
runner.run(suite())
Здесь:
- Мы создали две группы тестов
TestMath
иTestStringMethods
. - Метод
suite()
создает набор тестов и добавляет в него отдельные тестовые методы. unittest.TextTestRunner
используется для запуска тестового набора.
Организация тестов в проекте
Для больших проектов обычно создают отдельные файлы и папки для тестов. Например:
my_project/
│
├── my_module.py
├── tests/
│ ├── __init__.py
│ ├── test_my_module.py
В файле test_my_module.py
вы можете разместить тесты для модуля my_module.py
. Таким образом, все тесты будут организованы и легко управляемы.
Запуск тестов
Вы можете запускать тесты несколькими способами:
- Запустить файл тестов напрямую:
python test_my_module.py
- Использовать встроенную команду:
python -m unittest discover
Эта команда автоматически найдет и выполнит все тесты в проекте, которые находятся в файлах, начинающихся на test_
.
Исключения и работа с ними в unittest
Проверка того, как ваш код работает с исключениями, является важной частью тестирования. В unittest
для этого существует несколько механизмов. Мы уже видели, как использовать метод assertRaises
для проверки того, что вызывается конкретное исключение. Но также существует более гибкий контекстный менеджер для проверки исключений, который позволяет детальнее протестировать текст сообщения об ошибке.
Пример:
import unittest
def divide(a, b):
if b == 0:
raise ValueError("Деление на ноль!")
return a / b
class TestDivision(unittest.TestCase):
def test_divide_by_zero(self):
# Проверяем, что выбрасывается исключение с определенным сообщением
with self.assertRaises(ValueError) as context:
divide(10, 0)
self.assertEqual(str(context.exception), "Деление на ноль!")
if __name__ == '__main__':
unittest.main()
Здесь:
- Мы используем контекстный менеджер
with self.assertRaises
, который позволяет захватить исключение и проверить его свойства (например, сообщение об ошибке).
Пропуск тестов и условное выполнение
В некоторых случаях вам может потребоваться пропустить тест или выполнить его только при определенных условиях. В unittest
есть декораторы, которые позволяют гибко управлять этим поведением:
@unittest.skip(reason)
— пропускает тест с указанием причины.@unittest.skipIf(condition, reason)
— пропускает тест, если условие истинно.@unittest.skipUnless(condition, reason)
— пропускает тест, если условие ложно.
Пример:
import unittest
import sys
class TestSkipping(unittest.TestCase):
@unittest.skip("Тест временно пропущен")
def test_temporary_skip(self):
self.assertEqual(1 + 1, 3)
@unittest.skipIf(sys.version_info < (3, 8), "Требуется Python 3.8 или выше")
def test_python_version(self):
self.assertEqual(2 ** 3, 8)
if __name__ == '__main__':
unittest.main()
Здесь:
- Первый тест будет пропущен с комментарием "Тест временно пропущен".
- Второй тест будет выполнен только если версия Python 3.8 или выше.
Параметризация тестов
Иногда требуется протестировать одну и ту же функцию с разными наборами данных. Для этого удобно использовать параметризацию тестов. Хотя в стандартном unittest
нет встроенной поддержки параметризации, это можно реализовать с помощью цикла внутри теста или используя внешние библиотеки, такие как parameterized
или ddt
.
Пример без использования дополнительных библиотек:
import unittest
def multiply(a, b):
return a * b
class TestMultiply(unittest.TestCase):
def test_multiply_with_various_data(self):
test_cases = [
(2, 3, 6),
(1, 5, 5),
(0, 10, 0),
(-1, 10, -10),
]
for a, b, expected in test_cases:
with self.subTest(a=a, b=b, expected=expected):
self.assertEqual(multiply(a, b), expected)
if __name__ == '__main__':
unittest.main()
В этом примере:
- Мы используем метод
subTest()
, который позволяет запускать отдельные под-тесты для каждого набора данных. Если один под-тест провалится, другие продолжат выполнение.
Моки и заглушки: использование unittest.mock
Часто при тестировании приходится заменять реальные объекты на фейковые или заглушки. Например, вы можете захотеть замокировать работу с базой данных или сетевое взаимодействие. Библиотека unittest
содержит модуль mock
, который помогает создавать такие заглушки.
Основные функции:
patch()
— используется для замены реального объекта мок-объектом.MagicMock
— предоставляет более гибкие возможности для создания мок-объектов, которые могут имитировать поведение реальных объектов.
Пример использования мока:
from unittest import TestCase
from unittest.mock import patch
# Функция, которая использует внешнюю зависимость
def get_user_data_from_api(user_id):
import requests
response = requests.get(f'https://api.example.com/users/{user_id}')
return response.json()
class TestUserData(TestCase):
@patch('requests.get')
def test_get_user_data_from_api(self, mock_get):
# Определяем поведение мок-объекта
mock_get.return_value.json.return_value = {'id': 1, 'name': 'John Doe'}
# Тестируем функцию
user_data = get_user_data_from_api(1)
self.assertEqual(user_data['name'], 'John Doe')
if __name__ == '__main__':
unittest.main()
Здесь:
- Мы используем
patch()
для замены функцииrequests.get
на мок-объект. - Мок-объект настроен на возвращение предопределенного результата — словаря с данными пользователя.
- Это позволяет протестировать функцию без фактического обращения к внешнему API.
Создание собственных утверждений (assertions)
Если стандартных методов утверждений недостаточно, вы можете легко создать свои собственные. Это удобно, когда часто повторяются одни и те же проверки.
Пример создания пользовательского утверждения:
import unittest
class TestCustomAssertions(unittest.TestCase):
def assertIsEven(self, number):
if number % 2 != 0:
self.fail(f'{number} is not an even number')
def test_even_numbers(self):
self.assertIsEven(4)
self.assertIsEven(10)
def test_odd_number(self):
with self.assertRaises(AssertionError):
self.assertIsEven(3)
if __name__ == '__main__':
unittest.main()
Здесь:
- Метод
assertIsEven
проверяет, является ли число четным. - В случае ошибки метод вызывает
self.fail()
, который генерирует исключениеAssertionError
.
Тестирование времени выполнения кода
Иногда необходимо убедиться, что функция выполняется за определенное время. Хотя unittest
не предоставляет встроенных средств для измерения времени выполнения, это можно легко реализовать с помощью модуля time
.
Пример:
import unittest
import time
def slow_function():
time.sleep(2)
class TestPerformance(unittest.TestCase):
def test_function_execution_time(self):
start_time = time.time()
slow_function()
elapsed_time = time.time() - start_time
self.assertLess(elapsed_time, 3, "Функция работает слишком медленно")
if __name__ == '__main__':
unittest.main()
В этом примере:
- Мы измеряем время выполнения функции с помощью
time.time()
. - Метод
assertLess
проверяет, что время выполнения меньше 3 секунд.
Генерация отчетов и запуск тестов с различными форматами
unittest
по умолчанию генерирует текстовые отчеты, но для интеграции с системами CI/CD часто требуются более формализованные отчеты в формате XML или HTML. Для этого существуют дополнительные библиотеки, такие как unittest-xml-reporting
или HtmlTestRunner
.
Пример использования HtmlTestRunner
:
- Установите библиотеку:
pip install html-testRunner
- Используйте ее для запуска тестов:
import unittest
import HtmlTestRunner
class TestExample(unittest.TestCase):
def test_example(self):
self.assertEqual(1 + 1, 2)
if __name__ == '__main__':
unittest.main(testRunner=HtmlTestRunner.HTMLTestRunner(output='example_dir'))
Этот код сгенерирует HTML-отчет в папке example_dir
после выполнения тестов.
Модуль unittest
предоставляет все необходимые инструменты для создания и выполнения модульных тестов в Python. Он легко расширяется, поддерживает моки и заглушки, позволяет организовывать тестовые наборы и предлагает гибкую систему утверждений. Важно помнить, что регулярное тестирование и наличие качественного набора тестов значительно упрощает разработку, сопровождение и рефакторинг вашего проекта.