B прошлой статье мы рассказывали о организации модулей Python, особенно полезных для крупных Data Science проектов. В этот раз поговорим о модульном тестировании (unit testing): читайте в нашей статье о том, как писать и запускать тесты для проверок функций и как в стандартной библиотеке Python применяется один из главных принципов разработки ПО — DRY (don’t repeat yourself).
Пишем простые тесты для функций модуля
Для тестирования будем использовать стандартную библиотеку unittest
. Допустим, имеется Python-файл с двумя функциями:
# файл calc.py def add(x, y): return x + y def is_positive(x): return x > 0
Требуется написать тесты для этого файла (модуля), который проверит правильность разработанных функций. Для этого создадим файл tests.py, в котором будет класс с методом для тестирования. Важно, чтобы все методы с тестами начинались с test_<что-то>
, иначе Python не поймет, что тестировать. После наследования от класса TestCase
из unittest
будут доступны методы, которые проверяют на соответствие ожидаемому результату. Напишем несколько проверок для функций вышеприведённого модуля:
# файл tests.py import unittest import calc # тестируемый модуль class TestCalc(unittest.TestCase): # начинается с test_ def test_add(self): self.assertEqual(calc.add(3, 6), 9) # начинается с test_ def test_is_positive(self): self.assertTrue(calc.is_positive(1))
Здесь два теста: один проверяет на равенство, другой на истину. Кроме того, имеются и другие виды проверок, которые показаны на рисунке ниже. Так, например, для сравнений чисел с плавающей точкой (float) рекомендуется использовать assertAlomstEqual
.
Запуск unit-тестов
Для запуска тестов нужно написать в командой строке следующее:
$ python -m unittest tests.py
На экране выведется сообщение о проведении тестов и их статус. Каждый пройденный тест обозначается точкой .
, а проваленной буквой F
. В нашем случае получили две точки и статус ОК
:
.. ---------------------------------------------------------------------- Ran 2 tests in 0.000s OK
Кроме того, чтобы не прописывать всю вышеприведённую строчку, можно добавить в Python-файл с тестами вызов функции unittest.main
в конце в блоке __main__
, о котором говорили тут:
# файл tests.py class TestCalc(unittest.TestCase) # остальной код if __name__ == "__main__": unittest.main()
Тогда запуск осуществляется простой командой:
$ python tests.py
Проваленные тесты
Попробуем заменить некоторые значения так, что ожидаемый результат не будет сходиться с вычислениями, например, изменим тест с проверкой на положительное число:
def test_is_positive(self): self.assertTrue(calc.is_positive(-1))
Ниже показано сообщение. В результате мы провалили один тест и получили .F
и статус Failed
.
.F ====================================================================== FAIL: test_is_positive (__main__.TestCalc) ---------------------------------------------------------------------- Traceback (most recent call last): File "tests.py", line 10, in test_is_positive self.assertTrue(calc.is_positive(-1)) AssertionError: False is not true ---------------------------------------------------------------------- Ran 2 tests in 0.001s FAILED (failures=1)
Если мы заменим в первом методе с проверкой на равенство ожидаемый результат на другое число, то провалим оба тести и получим FF
. Ниже показано, как это выглядит.
def test_add(self): self.assertEqual(calc.add(3, 6), 10)
FF ====================================================================== FAIL: test_add (__main__.TestCalc) ---------------------------------------------------------------------- AssertionError: 9 != 10 ====================================================================== FAIL: test_is_positive (__main__.TestCalc) ---------------------------------------------------------------------- AssertionError: False is not true FAILED (failures=2)
Тестирование исключений
На практике может возникнуть ситуация, когда функция содержит исключение, срабатываемое при определенных условиях. Их тоже можно проверять, ведь, если функция Python поднимает исключение при ожидаемом результате, значит, она работает корректно.
Добавим в файл с вычислениями ещё одну функцию деления одного числа на другое. Понятно, что делить на 0 невозможно, поэтому функция поднимает соответствующее исключение:
# Файл calc.py def divide(x, y): if y == 0: raise ZeroDivisionError return x / y
Тогда для проверки исключений рекомендуется использовать контекстный менеджер Python, внутри которого просто вызвать проверяемую функцию:
def test_divide(self): with self.assertRaises(ZeroDivisionError): calc.divide(10, 0)
Операции перед проведением тестов
Порой требуется выполнить операции перед каждым тестом, особенно, если требуется протестировать методы класса, не создавая постоянно экземпляров класса. Более того, это позволит соблюсти принцип DRY (Don’t Repeat Youreself — не повторяйся). Пусть в файле person.py имеется класс Person
, который хранит информацию о имени, фамилии и e-mail:
class Person: def __init__(self, first_name, last_name): self.first = first_name self.last = last_name @property def email(self): return f"{self.last}@school.ru" @property def full(self): return f"{self.first} {self.last}"
Воспользуемся методом setUp
, который перед каждым тестом будет создавать экземпляр класса Person. В итоге, проверим правильность введенного e-mail и полного имени. Ниже приведён код на Python.
class TestPerson(unittest.TestCase): @classmethod def setUpClass(cls): cls.p = Person("Vasya", "Frolov") def test_email(self): self.assertEqual(self.p.email, "Frolov@school.ru") def test_fullname(self): self.assertEqual(self.p.full, "Vasya Frolov")
Помимо setUp
, имеется метод tearDown
, который, наоборот, запускается в конце каждого теста. Эти методы также пригодятся для открытия и закрытия файлов.
В следующей статье поговорим о продвинутых темах модульного тестирования. А о том, как писать тесты в реальных проектах Data Science, вы узнаете на наших Python-курсах в лицензированном учебном центре обучения и повышения квалификации IT-специалистов в Москве.