Тестируем на Python: unittest и pytest. Инструкция для начинающих
Python-разработчик Андрей Смирнов рассказал, как написать первые тесты и какие фреймворки выбрать: unittest или pytest.
Меня зовут Андрей Смирнов, я занимаюсь Python-разработкой, автоматизацией технических процессов и преподаю промышленное программирование в Школе программистов МШП.
Не секрет, что разработчики создают программы, которые рано или поздно становятся очень масштабными (если смотреть на количество строчек кода). А с этим приходит и большая ответственность за качество.
Сейчас расскажу, как unittest и pytest помогут найти ошибки в программах и исключить их в будущем.
Итак, тестирование
Каждый, кто писал первые программы (будь то классический «hello, world» или же калькулятор), всегда запускал тесты, чтобы проверить их работу.
Сам факт запуска — самое первое, незримое касание технологии тестирования в вашей жизни. Рассмотрим его как процесс поиска ошибок на чуть более сложной программе.
Например, вам нужно ввести три числа (a, b, c) и найти корни квадратного уравнения. Для решения пишем код:
from math import sqrt def square_eq_solver(a, b, c): result = [] discriminant = b * b - 4 * a * c if discriminant == 0: result.append(-b / (2 * a)) else: result.append((-b + sqrt(discriminant)) / (2 * a)) result.append((-b - sqrt(discriminant)) / (2 * a)) return result def show_result(data): if len(data) > 0: for index, value in enumerate(data): print(f'Корень номер равен ') else: print('Уравнение с заданными параметрами не имеет корней') def main(): a, b, c = map(int, input('Пожалуйста, введите три числа через пробел: ').split()) result = square_eq_solver(a, b, c) show_result(result) if __name__ == '__main__': main()
Сразу оговорюсь: любую задачу, какой бы она ни была краткой, я рассматриваю с позиции «когда-нибудь она вырастет и станет очень объёмной». Поэтому всегда стараюсь разделять программу на различные подпрограммы (ввод/обработка/вывод).
Возможно, вы уже заметили ошибку в коде. Однако иногда она может быть скрыта настолько глубоко, что её просто так не обнаружишь. И в таком случае единственный способ вывести ее на свет — протестировать код. Как это сделать?
— зная алгоритм нахождения корней уравнения, определяем наборы входных данных, которые будут переданы на вход программе;
— зная входные данные, можно вручную просчитать, какой ответ должна дать программа;
— запускаем программу и передаем ей на вход исходные данные;
— получаем от нее ответ и сравниваем с тем, который должен быть получен. Если они совпадают — хорошо, идём к следующему набору данных, если нет, сообщаем об ошибке.
Например, для данной задачи можно подобрать следующие тесты:
- 10x**2 = 0 — единственный корень x=0
- 2x**2 + 5x — 3 = 0 — у такого уравнения два корня (x1 = 0.5, x2=-3)
- 10x**2+2 = 0 — у этого уравнения корней нет
Тесты подобрали, что дальше? Правильно, запускаем:
Тест номер 1 > python.exe example.py Пожалуйста, введите три числа через пробел: 10 0 0 Корень номер 0 равен 0.00 Тест номер 2: > python.exe example.py Пожалуйста, введите три числа через пробел: 2 5 -3 Корень номер 1 равен 0.50 Корень номер 2 равен -3.00 Тест номер 3: > python.exe example.py Пожалуйста, введите три числа через пробел: 10 0 2 Traceback (most recent call last): File "C:PyProjectstprogerexample.py", line 32, in main() File "C:PyProjectstprogerexample.py", line 27, in main result = square_eq_solver(a, b, c) File "C:PyProjectstprogerexample.py", line 11, in square_eq_solver result.append((-b + sqrt(discriminant)) / (2 * a)) ValueError: math domain error
Упс… В третьем тесте произошла ошибка. Как раз та, которую вы могли заметить в исходном коде программы — не обрабатывался случай с нулевым дискриминантом. В итоге, можно подкорректировать код функции так, чтобы этот вариант обрабатывался правильно:
def square_eq_solver(a, b, c): result = [] discriminant = b * b - 4 * a * c if discriminant == 0: result.append(-b / (2 * a)) elif discriminant > 0: #
Запускаем все тесты повторно и они срабатывают нормально.
Но учтите, чтобы повторно проверить программу, потребуется потратить несколько минут и снова проверить все три варианта входных значений. Если таких вариантов будет много, вызывать их вручную будет очень накладно. И здесь на сцену выходит автоматизированное тестирование.
Программа автоматического тестирования запускается на основе заранее заготовленных входных/выходных данных и программы, которая будет их вызывать. По сути, это программа, тестирующая другие программы. И в рамках экосистемы языка Python есть несколько пакетов, позволяющих автоматизировать процесс тестирования.
Unittest и pytest: пишем тесты
Две самые популярные библиотеки — unittest и pytest. Попробуем каждую, чтобы объективно оценить синтаксис.
Начнем с unittest, потому что именно с нее многие знакомятся с миром тестирования. Причина проста: библиотека по умолчанию встроена в стандартную библиотеку языка Python.
Формат кода
По формату написания тестов она сильно напоминает библиотеку JUnit, используемую в языке Java для написания тестов:
- тесты должны быть написаны в классе;
- класс должен быть отнаследован от базового класса unittest.TestCase;
- имена всех функций, являющихся тестами, должны начинаться с ключевого слова test;
- внутри функций должны быть вызовы операторов сравнения (assertX) — именно они будут проверять наши полученные значения на соответствие заявленным.
Пример использования unittest для нашей задачи
import unittest class SquareEqSolverTestCase(unittest.TestCase): def test_no_root(self): res = square_eq_solver(10, 0, 2) self.assertEqual(len(res), 0) def test_single_root(self): res = square_eq_solver(10, 0, 0) self.assertEqual(len(res), 1) self.assertEqual(res, [0]) def test_multiple_root(self): res = square_eq_solver(2, 5, -3) self.assertEqual(len(res), 2) self.assertEqual(res, [0.5, -3])
Запускается данный код следующей командой
python.exe -m unittest example.py
И в результате на экран будет выведено:
> python.exe -m unittest example.py . ------------------------------------------------------------------ Ran 3 tests in 0.001s OK
В случае, если в каком-нибудь из тестов будет обнаружена ошибка, unittest не замедлит о ней сообщить:
> python.exe -m unittest example.py F.. ================================================================== FAIL: test_multiple_root (hello.SquareEqSolverTestCase) ------------------------------------------------------------------ Traceback (most recent call last): File "C:PyProjectstprogerexample.py", line 101, in test_multiple_root self.assertEqual(len(res), 3) AssertionError: 2 != 3 ------------------------------------------------------------------ Ran 3 tests in 0.001s FAILED (failures=1)
Unittest: аргументы “за”
- Является частью стандартной библиотеки языка Python: не нужно устанавливать ничего дополнительно;
- Гибкая структура и условия запуска тестов. Для каждого теста можно назначить теги, в соответствии с которыми будем запускаться либо одна, либо другая группа тестов;
- Быстрая генерация отчетов о проведенном тестировании, как в формате plaintext, так и в формате XML.
Unittest: аргументы “против”
- Для проведения тестирования придётся написать достаточно большое количество кода (по сравнению с другими библиотеками);
- Из-за того, что разработчики вдохновлялись форматом библиотеки JUnit, названия основных функций написаны в стиле camelCase (например setUp и assertEqual);
- В языке python согласно рекомендациям pep8 должен использоваться формат названий snake_case (например set_up и assert_equal).
Pytest
Возможно, наиболее популярный фреймворк с открытым исходным кодом из всех, представленных здесь.
Pytest позволяет провести модульное тестирование (тестирование отдельных компонентов программы), функциональное тестирование (тестирование способности кода удовлетворять бизнес-требования), тестирование API (application programming interface) и многое другое.
Формат кода
Написание тестов здесь намного проще, нежели в unittest. Вам нужно просто написать несколько функций, удовлетворяющих следующим условиям:
- Название функции должно начинаться с ключевого слова test;
- Внутри функции должно проверяться логическое выражение при помощи оператора assert.
Пример использования pytest для нашей задачи:
def test_no_root(): res = square_eq_solver(10, 0, 2) assert len(res) == 0 def test_single_root(): res = square_eq_solver(10, 0, 0) assert len(res) == 1 assert res == [0] def test_multiple_root(): res = square_eq_solver(2, 5, -3) assert len(res) == 3 assert res == [0.5, -3]
Запускается данный код следующей командой
pytest.exe example.py
И в результате на экран будет выведено:
> pytest.exe example.py ======================= test session starts ====================== platform win32 -- Python 3.9.6, pytest-7.1.2, pluggy-1.0.0 rootdir: C:PyProjectstproger collected 3 items example.py . [100%] ======================== 3 passed in 0.03s =======================
В случае ошибки вывод будет несколько больше:
> pytest.exe example.py ======================= test session starts ====================== platform win32 -- Python 3.9.6, pytest-7.1.2, pluggy-1.0.0 rootdir: C:PyProjectstproger collected 3 items example.py ..F [100%] ============================ FAILURES ============================ _______________________ test_multiple_root _______________________ def test_multiple_root(): res = square_eq_solver(2, 5, -3) > assert len(res) == 3 E assert 2 == 3 E + where 2 = len([0.5, -3.0]) example.py:116: AssertionError ===================== short test summary info ==================== FAILED example.py::test_multiple_root - assert 2 == 3 =================== 1 failed, 2 passed in 0.10s ==================
Pytest: аргументы “за”
- Позволяет писать компактные (по сравнению с unittest) наборы тестов;
- В случае возникновения ошибок выводится гораздо больше информации о них;
- Позволяет запускать тесты, написанные для других тестирующих систем;
- Имеет систему плагинов (и сотни этих самых плагинов), расширяющую возможности фреймворка. Примеры таких плагинов: pytest-cov, pytest-django, pytest-bdd;
- Позволяет запускать тесты в параллели (при помощи плагина pytest-xdist).
Pytest: аргументы “против”
- pytest не входит в стандартную библиотеку языка Python. Поэтому его придётся устанавливать отдельно при помощи команды pip install pytest;
- совместимость кода с другими фреймворками отсутствует. Так что, если напишете код под pytest, запустить его при помощи встроенного unittest не получится.
Ну и что лучше?
- Если вам нужно базовое юнит-тестирование и вы знакомы с фреймворками вида xUnit, тогда вам подойдёт unittest.
- Если нужен фреймворк, позволяющий создавать краткие и изящные тесты, реализующие сложную логику проверок, то pytest.
Post Scriptum
Тема контроля качества очень обширна. И даже к написанному мной коду очень легко придраться. Как минимум здесь отсутствует проверка на то, что вводимые данные обязательно должны быть целыми числами. Если ввести любое другое число или даже строку, она обязательно завершится с ошибкой.
Кстати, в этой программе я намеренно оставил ещё одну ошибку (на сей раз уже логическую), связанную с нахождением корня. Напишите в комментариях, с чем она может быть связана, и какой тест поможет её отловить ?
Модуль unittest: тестируем свои программы
Представьте, что вы написали какую-либо программу, а теперь хотите проверить, правильно ли она работает. Что вы для этого сделаете? Скорее всего, вы запустите её несколько раз с различными входными данными, и убедитесь в правильности выдаваемого ответа.
А теперь вы что-то поменяли и снова хотите проверить корректность программы. Запускать ещё несколько раз? А если потом снова что-то поменяется? Нельзя ли как-то автоматизировать это дело?
Оказывается, можно. В Python встроен модуль unittest, который поддерживает автоматизацию тестов, использование общего кода для настройки и завершения тестов, объединение тестов в группы, а также позволяет отделять тесты от фреймворка для вывода информации.
Для автоматизации тестов, unittest поддерживает некоторые важные концепции:
- Испытательный стенд (test fixture) - выполняется подготовка, необходимая для выполнения тестов и все необходимые действия для очистки после выполнения тестов. Это может включать, например, создание временных баз данных или запуск серверного процесса.
- Тестовый случай (test case) - минимальный блок тестирования. Он проверяет ответы для разных наборов данных. Модуль unittest предоставляет базовый класс TestCase, который можно использовать для создания новых тестовых случаев.
- Набор тестов (test suite) - несколько тестовых случаев, наборов тестов или и того и другого. Он используется для объединения тестов, которые должны быть выполнены вместе.
- Исполнитель тестов (test runner) - компонент, который управляет выполнением тестов и предоставляет пользователю результат. Исполнитель может использовать графический или текстовый интерфейс или возвращать специальное значение, которое сообщает о результатах выполнения тестов.
Модуль unittest предоставляет богатый набор инструментов для написания и запуска тестов. Однако достаточно лишь некоторых из них, чтобы удовлетворить потребности большинства пользователей.
Вот короткий скрипт для тестирования трех методов строк:
Тестовый случай создаётся путём наследования от unittest.TestCase. 3 отдельных теста определяются с помощью методов, имя которых начинается на test. Это соглашение говорит исполнителю тестов о том, какие методы являются тестами.
Суть каждого теста - вызов assertEqual() для проверки ожидаемого результата; assertTrue() или assertFalse() для проверки условия; assertRaises() для проверки, что метод порождает исключение. Эти методы используются вместо обычного assert для того, чтобы исполнитель тестов смог взять все результаты и оформить отчёт.
Методы setUp() и tearDown() (которые в данном простом случае не нужны) позволяют определять инструкции, выполняемые перед и после каждого теста, соответственно.
Последние 2 строки показывают простой способ запуска тестов. unittest.main() предоставляет интерфейс командной строки для тестирования программы. Будучи запущенным из командной строки, этот скрипт выводит отчёт, подобный этому:
unittest может быть использован из командной строки для запуска модулей с тестами, классов или даже отдельных методов:
python -m unittest test_module1 test_module2 python -m unittest test_module.TestClass python -m unittest test_module.TestClass.test_method
Можно также указывать путь к файлу:
python -m unittest tests/test_something.py
С помощью флага -v можно получить более детальный отчёт:
python -m unittest -v test_module
Для нашего примера подробный отчёт будет таким:
-b ( --buffer ) - вывод программы при провале теста будет показан, а не скрыт, как обычно.
-c ( --catch ) - Ctrl+C во время выполнения теста ожидает завершения текущего теста и затем сообщает результаты на данный момент. Второе нажатие Ctrl+C вызывает обычное исключение KeyboardInterrupt.
-f ( --failfast ) - выход после первого же неудачного теста.
--locals (начиная с Python 3.5) - показывать локальные переменные для провалившихся тестов.
Обнаружение тестов
unittest поддерживает простое обнаружение тестов. Для совместимости с обнаружением тестов, все файлы тестов должны быть модулями или пакетами, импортируемыми из директории верхнего уровня проекта (см. подробнее о правилах наименования модулей ).
Обнаружение тестов реализовано в TestLoader.discover(), но может быть использовано из командной строки:
-v ( --verbose ) - подробный вывод.
-s ( --start-directory ) directory_name - директория начала обнаружения тестов (текущая по умолчанию).
-p ( --pattern ) pattern - шаблон названия файлов с тестами (по умолчанию test*.py).
-t ( --top-level-directory ) directory_name - директория верхнего уровня проекта (по умолчанию равна start-directory ).
Организация тестового кода
Базовые блоки тестирования это тестовые случаи - простые случаи, которые должны быть проверены на корректность.
Тестовый случай создаётся путём наследования от unittest.TestCase.
Тестирующий код должен быть самостоятельным, то есть никак не зависеть от других тестов.
Простейший подкласс TestCase может просто реализовывать тестовый метод (метод, начинающийся с test). Вымышленный пример:
Заметьте, что для того, чтобы проверить что-то, мы используем один из assert\*() методов.
Тестов может быть много, и часть кода настройки может повторяться. К счастью, мы можем определить код настройки путём реализации метода setUp(), который будет запускаться перед каждым тестом:
Мы также можем определить метод tearDown(), который будет запускаться после каждого теста:
Можно разместить все тесты в том же файле, что и сама программа (таком как widgets.py), но размещение тестов в отдельном файле (таком как test_widget.py) имеет много преимуществ:
- Модуль с тестом может быть запущен автономно из командной строки.
- Тестовый код может быть легко отделён от программы.
- Меньше искушения изменить тесты для соответствия коду программы без видимой причины.
- Тестовый код должен изменяться гораздо реже, чем программа.
- Протестированный код может быть легче переработан.
- Тесты для модулей на C должны быть в отдельных модулях, так почему же не быть последовательным?
- Если стратегия тестирования изменяется, нет необходимости изменения кода программы.
Пропуск тестов и ожидаемые ошибки
unittest поддерживает пропуск отдельных тестов, а также классов тестов. Вдобавок, поддерживается пометка теста как "не работает, но так и надо".
Пропуск теста осуществляется использованием декоратора skip() или одного из его условных вариантов.
Классы также могут быть пропущены:
Ожидаемые ошибки используют декоратор expectedFailure():
Очень просто сделать свой декоратор. Например, следующий декоратор пропускает тест, если переданный объект не имеет указанного атрибута:
Декораторы, пропускающие тесты или говорящие об ожидаемых ошибках:
@unittest.skip(reason) - пропустить тест. reason описывает причину пропуска.
@unittest.skipIf(condition, reason) - пропустить тест, если condition истинно.
@unittest.skipUnless(condition, reason) - пропустить тест, если condition ложно.
@unittest.expectedFailure - пометить тест как ожидаемая ошибка.
Для пропущенных тестов не запускаются setUp() и tearDown(). Для пропущенных классов не запускаются setUpClass() и tearDownClass(). Для пропущенных модулей не запускаются setUpModule() и tearDownModule().
Различение итераций теста с помощью подтестов
Когда некоторые тесты имеют лишь незначительные отличия, например некоторые параметры, unittest позволяет различать их внутри одного тестового метода, используя менеджер контекста subTest().
Например, следующий тест:
даст следующий отчёт:
Без использования подтестов, выполнение будет остановлено после первой ошибки, и ошибку будет сложнее диагностировать, потому что значение i не будет показано:
Модуль unittest предоставляет множество функций для самых различных проверок:
assertTrue(x) — bool(x) is True
assertFalse(x) — bool(x) is False
assertIsNot(a, b) — a is not b
assertIsNone(x) — x is None
assertIsNotNone(x) — x is not None
assertNotIn(a, b) — a not in b
assertIsInstance(a, b) — isinstance(a, b)
assertNotIsInstance(a, b) — not isinstance(a, b)
assertRaises(exc, fun, *args, **kwds) — fun(*args, **kwds) порождает исключение exc
assertRaisesRegex(exc, r, fun, *args, **kwds) — fun(*args, **kwds) порождает исключение exc и сообщение соответствует регулярному выражению r
assertWarns(warn, fun, *args, **kwds) — fun(*args, **kwds) порождает предупреждение
assertWarnsRegex(warn, r, fun, *args, **kwds) — fun(*args, **kwds) порождает предупреждение и сообщение соответствует регулярному выражению r
assertAlmostEqual(a, b) — round(a-b, 7) == 0
assertNotAlmostEqual(a, b) — round(a-b, 7) != 0
assertGreaterEqual(a, b) — a >= b
assertNotRegex(s, r) — not r.search(s)
assertCountEqual(a, b) — a и b содержат те же элементы в одинаковых количествах, но порядок не важен
Для вставки кода на Python в комментарий заключайте его в теги
- Модуль csv - чтение и запись CSV файлов
- Создаём сайт на Django, используя хорошие практики. Часть 1: создаём проект
- Онлайн-обучение Python: сравнение популярных программ
- Книги о Python
- GUI (графический интерфейс пользователя)
- Курсы Python
- Модули
- Новости мира Python
- NumPy
- Обработка данных
- Основы программирования
- Примеры программ
- Типы данных в Python
- Видео
- Python для Web
- Работа для Python-программистов
- Сделай свой вклад в развитие сайта!
- Самоучитель Python
- Карта сайта
- Отзывы на книги по Python
- Реклама на сайте
Юнит-тесты на Python: Быстрый старт

Юнит-тестирование кода является неотъемлемой частью жизненного цикла разработки программного обеспечения. Юнит-тесты также формируют основу для проведения регрессионного тестирования, то есть они гарантируют, что система будет вести себя согласно сценарию, когда добавятся новые функциональные возможности или изменятся существующие.
В этой статье я продемонстрирую основную идею юнит-тестирования на одном классе. На практике вам придется писать множество тестовых случаев, добавлять их в тестовый набор и запускать все вместе. Управление тест-кейсами мы рассмотрим в следующей статье.
Сегодня мы сосредоточимся на тестировании бэкенда. То есть разработчик реализовал некоторый проект согласно спецификациям (например, Calculator.py), а ваша задача состоит в том, чтобы убедиться, что разработанный код действительно им соответствует (например, с помощью TestCalculator.py ).
Предположим, что вы написали класс Calculator для выполнения основных вычислительных функций: сложения, вычитания, умножения и деления.
Код для этого здесь ( Calculator.py ):
#A simple calculator class Calculator: #empty constructor def __init__(self): pass #add method - given two numbers, return the addition def add(self, x1, x2): return x1 + x2 #multiply method - given two numbers, return the #multiplication of the two def multiply(self, x1, x2): return x1 * x2 #subtract method - given two numbers, return the value #of first value minus the second def subtract(self, x1, x2): return x1 - x2 #divide method - given two numbers, return the value #of first value divided by the second def divide(self, x1, x2): if x2 != 0: return x1/x2
Теперь я хочу запустить юнит-тест, чтобы понять, что функциональность в приведенном выше классе работает, как запланировано.
Обычно Python поставляется уже с пакетом unittest . Если в вашей системе его нет, используйте pip для его установки.
Юнит-тест имеет следующую структуру:

setUp() и tearDown() – это стандартные методы, которые поставляются с фреймворком unittest (они определены в классе unittest.TestCase). В зависимости от вашего тестового случая вы можете переопределять или не переопределять два этих метода по умолчанию.
Пришло время посмотреть на код тест-кейса. Вот файл TestCalculator.py .
import unittest from Calculator import Calculator #Test cases to test Calulator methods #You always create a child class derived from unittest.TestCase class TestCalculator(unittest.TestCase): #setUp method is overridden from the parent class TestCase def setUp(self): self.calculator = Calculator() #Each test method starts with the keyword test_ def test_add(self): self.assertEqual(self.calculator.add(4,7), 11) def test_subtract(self): self.assertEqual(self.calculator.subtract(10,5), 5) def test_multiply(self): self.assertEqual(self.calculator.multiply(3,7), 21) def test_divide(self): self.assertEqual(self.calculator.divide(10,2), 5) # Executing the tests in the above test case class if __name__ == "__main__": unittest.main()
Хотя это и не обязательно, но как правило я называю тестовый класс с префиксом Test (в нашем случае TestCalculator). Ключевым требованием в этом классе является наличие надкласса unittest.TestCase .
Всякий раз, когда выполняется этот тест-кейс, сначала выполняется метод setUp(). В нашем случае мы просто создаем объект класса Calculator и сохраняем его как атрибут класса. В родительском классе есть несколько других методов по умолчанию, которые мы рассмотрим позже.
На данный момент все, что вы будете делать, это писать методы test_xxx для тестирования каждого метода в классе Calculator. Обратите внимание, что все тестовые методы начинаются с префикса test_ . Это говорит Python с помощью фреймворка unittest, что это методы тестирования.
В каждом из методов тестирования я использовал встроенный метод assertEqual , чтобы проверить, возвращают ли методы калькулятора ожидаемое значение. Если возвращаемое значение равно ожидаемому значению, то тест проходит успешно, в противном случае он завершается неудачей.
Есть множество встроенных методов assert , о которых мы будем говорить позже.
Последняя строка в приведенном выше коде просто запускает тестовый случай TestCalculator . Он выполняет каждый тестовый метод, определенный внутри класса, и выдает результат.
python TestCalculator.py -v
Вы увидите вывод, сходный со следующим:
test_add (__main__.TestCalculator) . ok test_divide (__main__.TestCalculator) . ok test_multiply (__main__.TestCalculator) . ok test_subtract (__main__.TestCalculator) . ok -------------------------------------------------------------------- Ran 4 tests in 0.000s OK
Что делать, если что-то не работает, как ожидалось? Давайте изменим ожидаемое значение test_divide с 5 на 6 (5 – правильное значение, сейчас мы посмотрим, что случится при сбое. Это не ошибка в исходном коде, а ошибка в тестовом наборе, у вас тоже могут быть ошибки в тестовых наборах, поэтому всегда проверяйте тестовые сценарии на наличие ошибок!)
import unittest from Calculator import Calculator #Test cases to test Calulator methods #You always create a child class derived from unittest.TestCase class class TestCalculator(unittest.TestCase): #setUp method overridden from the parent class TestCase def setUp(self): self.calculator = Calculator() . def test_divide(self): self.assertEqual(self.calculator.divide(10,2), 6) # Executing the tests in the above test case class if __name__ == "__main__": unittest.main()
При запуске этого тест-кейса, вы получите следующий результат:
test_add (__main__.TestCalculator) . ok test_divide (__main__.TestCalculator) . FAIL test_multiply (__main__.TestCalculator) . ok test_subtract (__main__.TestCalculator) . ok ==================================================================== FAIL: test_divide (__main__.TestCalculator) -------------------------------------------------------------------- Traceback (most recent call last): File "TestCalculator.py", line 23, in test_divide self.assertEqual(self.calculator.divide(10,2), 6) AssertionError: 5.0 != 6 -------------------------------------------------------------------- Ran 4 tests in 0.001s FAILED (failures=1)
Здесь написано, что 3 из 4 тестов прошли успешно, а один не удался. В реальном сценарии, предполагается, что ваш тестовый случай является верным, то есть таким образом он помогает определять неправильно реализованную функцию.
- Блог компании OTUS
- Python
- Тестирование веб-сервисов
Тестирование в Python [unittest]. Часть 2. TestCase
![]()
Продолжаем изучать unittest – framework для тестирования в Python. В этой статье сосредоточимся на общем обзоре основных структурных элементов unittest (test case, test suite, test runner), рассмотрим способы запуска тестов и подробно остановимся на классе TestCase.
- Основные структурные элементы unittest.
- Запуск тестов из командной строки и с использованием графического приложения.
- Работа с TestCase.
Основные структурные элементы unittest
unittest – это framework для тестирования в Python, который позволяет разрабатывать автономные тесты, собирать тесты в коллекции, обеспечивает независимость тестов от framework’а отчетов и т.д. Основными структурными элемента каркаса unittest являются:
Test fixture
Test fixture – обеспечивает подготовку окружения для выполнения тестов, а также организацию мероприятий по их корректному завершению (например очистка ресурсов). Подготовка окружения может включать в себя создание баз данных, запуск необходим серверов и т.п.
Test case
Test case – это элементарная единица тестирования, в рамках которой проверяется работа компонента тестируемой программы (метод, класс, поведение и т.п.). Для реализации этой сущности используется класс TestCase.
Test suite
Test suite – это коллекция тестов, которая может в себя включать как отдельные test case’ы так и целые коллекции (т.е. можно создавать коллекции коллекций). Коллекции используются с целью объединения тестов для совместного запуска.
Test runner
Test runner – это компонент, которые оркестрирует (координирует взаимодействие) запуск тестов и предоставляет пользователю результат их выполнения. Test runner может иметь графический интерфейс, текстовый интерфейс или возвращать какое-то заранее заданное значение, которое будет описывать результат прохождения тестов.
Вся работа по написанию тестов заключается в том, что мы разрабатываем отдельные тесты в рамках test case’ов, собираем их в модули и запускаем, если нужно объединить несколько test case’ов, для их совместного запуска, они помещаются в test suite’ы, которые помимо test case’ов могут содержать другие test suite’ы.
Запуск тестов из командной строки и с использованием графического приложения
Запуск тестов можно сделать как из командной строки, так и с помощью графического интерфейса пользователя (GUI), рассмотрим каждый из этих способов более подробно. В качестве примера приложения, будет выступать utest_calc.py из предыдущей статьи.
Интерфейс командной строки (CLI)
CLI позволяет запускать тесты из целого модуля, класса или даже обращаться к конкретному тесту.
Запуск всех тестов в модуле utest_calc.py.
> python -m unittest test_calc.py
Запуск тестов из класса CalcTest.
> python -m unittest utest_calc.CalcTest
Запуск теста test_sub().
> python -m unittest utest_calc.CalcTest.test_sub
Как уже было сказано в первой части данного цикла, для вывода подробной информации необходимо добавить ключ -v.
> python -m unittest -v utest_calc.py
Если осуществить запуск без указания модуля с тестами, будет запущен Test Discovery.
> python -m unittest
Более подробно о Test Discovery будет рассказано в одной из следующих частей. С правку по ключам запуска можно получить из документации.
Графический интерфейс пользователя (GUI)
Для запуска и анализа результатов работы тестов можно использовать GUI. Списко инструментов доступен здесь , но он далеко не полный. Для пример рассмотрим работу с Cricket . Для установки Cricket можно воспользоваться менеджером pip:
> pip install cricket
После этого на ваш компьютер будет установлен cricket-unittest.
Для запуска тестов в данном приложении, перейдите в каталог с вашим тестирующим кодом и в командной строке запустите cricket-unittest, для этого просто наберите это название и нажмите Enter.
> cricket-unittest
Приложение, при запуске, автоматически загрузит тесты.

Для запуска тестов нажмите “Run all”. Как видно, все тесты завершились удачно – они окрасились в зеленый цвет.

Работа с TestCase
Как уже было сказано – основным строительным элементом при написании тестов с использованием unittest является TestCase. Он представляет собой класс, который должен являться базовым для всех остальных классов, методы которых будут тестировать те или иные автономные единицы исходной программы.
Приведем еще раз класс CalcTest из предыдущего урока:
import unittest import calc class CalcTests(unittest.TestCase): def test_add(self): self.assertEqual(calc.add(1, 2), 3) def test_sub(self): self.assertEqual(calc.sub(4, 2), 2) def test_mul(self): self.assertEqual(calc.mul(2, 5), 10) def test_div(self): self.assertEqual(calc.div(8, 4), 2) if __name__ == '__main__': unittest.main()
Для того, чтобы была возможность использовать компоненты unittest (в том числе и TestCase), в самом начале программы нужно импортировать модуль unittest стандартным образом.
При выборе имени класса наследника от TestCase можете руководствоваться следующим правилом: [ИмяТестируемойСущности]Tests. [ИмяТестируемойСущности] – это некоторая логическая единица, тесты для которой нужно написать. В нашем случае – это калькулятор, поэтому мы выбрали имя CalcTests. Если бы у нашего калькулятора был большой набор поддерживаемых функций, то тестирование простых функций (сложение, вычитание, умножение и деление) можно было бы вынести в отдельный класс и назвать его например так: CalcSimpleActionsTests. При написании программ на Python старайтесь придерживаться PEP 8 — Style Guide for Python Code – это рекомендации по стилевому оформлению кода.
Для того, чтобы метод класса выполнялся как тест, необходимо, чтобы он начинался со слова test. Несмотря на то, что методы framework’а unittest написаны не в соответствии с PEP 8 (ввиду того, что идейно он наследник xUnit), мы все же рекомендуем следовать правилам стиля для Python везде, где это возможно. Поэтому имена тестов будем начинать с префикса test_. Далее, под словом тест будем понимать метод класса-наследника от TestCase, который начинается с префикса test_.
Все методы класса TestCase можно разделить на три группы:
- методы, используемые при запуске тестов;
- методы, используемые при непосредственном написании тестов (проверка условий, сообщение об ошибках);
- методы, позволяющие собирать информацию о самом тесте.
Рассмотрим методы этих групп более подробно. Остановимся только на тех методах, которые могут быть полезны в первую очередь, при разработке тестов. За более подробной информацией отправляем на сайт с официальной документацией.
Методы, используемые при запуске тестов.
К этим методам относятся:
Метод вызывается перед запуском теста. Как правило, используется для подготовки окружения для теста.
tearDown()
Метод вызывается после завершения работы теста. Используется для “приборки” за тестом.
Заметим, что методы setUp() и tearDown() вызываются для всех тестов в рамках класса, в котором они переопределены. По умолчанию, эти методы ничего не делают. Если их добавить в utest_calc.py, то перед [после] тестов test_add(), test_sub(), test_mul(), test_div() будут выполнены setUp() [tearDown()].

setUpClass()
Метод действует на уровне класса, т.е. выполняется перед запуском тестов класса. При этом синтаксис требует наличие декоратора @classmethod.
@classmethod def setUpClass(cls): .
tearDownClass()
Запускается после выполнения всех методов класса, требует наличия декоратора @classmethod.
@classmethod def tearDownClass(cls): .
skipTest(reason)
Данный метод может быть использован для пропуска теста, если это необходимо.
Методы, используемые при непосредственном написании тестов (проверка условий, сообщение об ошибках).
TestCase класс предоставляет набор assert-методов для проверки и генерации ошибок:
| assertEqual(a, b) | a == b |
| assertNotEqual(a, b) | a != b |
| assertTrue(x) | bool(x) is True |
| assertFalse(x) | bool(x) is False |
| assertIs(a, b) | a is b |
| assertIsNot(a, b) | a is not b |
| assertIsNone(x) | x is None |
| assertIsNotNone(x) | x is not None |
| assertIn(a, b) | a in b |
| assertNotIn(a, b) | a not in b |
| assertIsInstance(a, b) | isinstance(a, b) |
| assertNotIsInstance(a, b) | not isinstance(a, b) |
Assert’ы для контроля выбрасываемых исключений и warning’ов:
| assertRaises(exc, fun, *args, **kwds) | Функция fun(*args, **kwds) вызывает исключение exc |
| assertRaisesRegex(exc, r, fun, *args, **kwds) | Функция fun(*args, **kwds) вызывает исключение exc, сообщение которого совпадает с регулярным выражением r |
| assertWarns(warn, fun, *args, **kwds) | Функция fun(*args, **kwds) выдает сообщение warn |
| assertWarnsRegex(warn, r, fun, *args, **kwds) | Функция fun(*args, **kwds) выдает сообщение warn и оно совпадает с регулярным выражением r |
Assert’ы для проверки различных ситуаций:
| assertAlmostEqual(a, b) | round(a-b, 7) == 0 |
| assertNotAlmostEqual(a, b) | round(a-b, 7) != 0 |
| assertGreater(a, b) | a > b |
| assertGreaterEqual(a, b) | a >= b |
| assertLess(a, b) | a < b |
| assertLessEqual(a, b) | a |
| assertRegex(s, r) | r.search(s) |
| assertNotRegex(s, r) | not r.search(s) |
| assertCountEqual(a, b) | a и b имеют одинаковые элементы (порядок неважен) |
Типо-зависимые assert’ы, которые используются при вызове assertEqual() . Приводятся на тот случай, если необходимо использовать конкретный метод.
| assertMultiLineEqual(a, b) | строки (strings) |
| assertSequenceEqual(a, b) | последовательности (sequences) |
| assertListEqual(a, b) | списки (lists) |
| assertTupleEqual(a, b) | кортежи (tuplse) |
| assertSetEqual(a, b) | множества или неизменяемые множества (frozensets) |
| assertDictEqual(a, b) | словари (dicts) |
Дополнительно хотелось бы отметить метод fail().
fail(msg=None)
Этот метод сигнализирует о том, что произошла ошибка в тесте.
Методы, позволяющие собирать информацию о самом тесте.
countTestCases()
Возвращает количество тестов в объекте класса-наследника от TestCase.
Возвращает строковый идентификатор теста. Как правило это полное имя метода, включающее имя модуля и имя класса.
shortDescription()
Возвращает описание теста, которое представляет собой первую строку docstring’а метода, если его нет, то возвращает None.
Расширим код нашего тестового проекта utest_calc.py, так чтобы показать некоторые из возможностей, которые предоставляет класс TestCase.
import unittest import calc class CalcTest(unittest.TestCase): """Calc tests""" @classmethod def setUpClass(cls): """Set up for class""" print("setUpClass") print("========= color: #5a525f; font-style: italic;">@classmethod def tearDownClass(cls): """Tear down for class""" print("========= color: #794938;">print("tearDownClass") def setUp(self): """Set up for test""" print("Set up for [" + self.shortDescription() + "]") def tearDown(self): """Tear down for test""" print("Tear down for [" + self.shortDescription() + "]") print("") def test_add(self): """Add operation test""" print("id: " + self.id()) self.assertEqual(calc.add(1, 2), 3) def test_sub(self): """Sub operation test""" print("id: " + self.id()) self.assertEqual(calc.sub(4, 2), 2) def test_mul(self): """Mul operation test""" print("id: " + self.id()) self.assertEqual(calc.mul(2, 5), 10) def test_div(self): """Div operation test""" print("id: " + self.id()) self.assertEqual(calc.div(8, 4), 2) if __name__ == '__main__': unittest.main()
Запустив это модуль в командной строке:
> python -m unittest -v utest_calc.py
Получим следующий результат:
setUpClass ========== test_add (simple_ex.CalcTest) Add operation test . Set up for [Add operation test] id: simple_ex.CalcTest.test_add Tear down for [Add operation test] ok test_div (simple_ex.CalcTest) Div operation test . Set up for [Div operation test] id: simple_ex.CalcTest.test_div Tear down for [Div operation test] ok test_mul (simple_ex.CalcTest) Mul operation test . Set up for [Mul operation test] id: simple_ex.CalcTest.test_mul Tear down for [Mul operation test] ok test_sub (simple_ex.CalcTest) Sub operation test . Set up for [Sub operation test] id: simple_ex.CalcTest.test_sub Tear down for [Sub operation test] ok ========== tearDownClass ---------------------------------------------------------------------- Ran 4 tests in 0.016s OK
Как видно из примера, вначале был запущен метод setUpClass(), потом последовательно (в алфавитном порядке) были выполнены тесты, перед запуском каждого теста выполнялся метод setUp(), по окончании – tearDown(). Каждый метод содержит docstring в виде комментария в первой строке. Для доступа к этому описанию использовался метод shortDescription(). В теле теста присутствует строка, печатающая идентификатор, получаемый с помощью функции id().
