Тестируем на Python: unittest и pytest. Инструкция для начинающих 

Аватарка пользователя Daria R

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'Корень номер {index+1} равен {value:.02f}')
   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 <module>
    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:  # <--- изменили условие, теперь
                           # при нулевом дискриминанте
                           # не будут вычисляться корни
       result.append((-b + sqrt(discriminant)) / (2 * a))
       result.append((-b - sqrt(discriminant)) / (2 * a))

   return result
		

Запускаем все тесты повторно и они срабатывают нормально.

Но учтите, чтобы повторно проверить программу, потребуется потратить несколько минут и снова проверить все три варианта входных значений. Если таких вариантов будет много, вызывать их вручную будет очень накладно. И здесь на сцену выходит автоматизированное тестирование.

Программа автоматического тестирования запускается на основе заранее заготовленных входных/выходных данных и программы, которая будет их вызывать. По сути, это программа, тестирующая другие программы. И в рамках экосистемы языка 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

Тема контроля качества очень обширна. И даже к написанному мной коду очень легко придраться. Как минимум здесь отсутствует проверка на то, что вводимые данные обязательно должны быть целыми числами. Если ввести любое другое число или даже строку, она обязательно завершится с ошибкой.

Кстати, в этой программе я намеренно оставил ещё одну ошибку (на сей раз уже логическую), связанную с нахождением корня. Напишите в комментариях, с чем она может быть связана, и какой тест поможет её отловить ?

Тестирование
Для начинающих
Python
Гостевая публикация
43560