Как писать модульные тесты на Python

Юнит-тестирование — это техника тестирования программного обеспечения (ПО), при которой отдельные компоненты или блоки приложения тестируются независимо от остальной части приложения.

При разработке ПО полезно разбивать приложение на небольшие изолированные части. Такой подход позволяет писать независимые тесты для проверки всех частей приложения, гарантируя, что они ведут себя так, как ожидается. А если тест не сработает, вы сможете легко изолировать и устранить ошибку в той части кода, в которой она проявилась, не затрагивая другие части.

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

В этой статье мы остановимся на использовании unittest.

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

Почему разработчики предпочитают использовать unittest?

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

Во-первых, модуль unittest является частью стандартной библиотеки Python. Это обеспечивает немедленную доступность и совместимость в различных средах без дополнительных зависимостей.

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

В-третьих, большинство интегрированных сред разработки (IDE), таких как PyCharm, предлагают встроенную поддержку unittest. Это повышает производительность разработчиков и упрощает процесс тестирования.

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

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

Как писать модульные тесты с помощью unittest

Юнит-тестирование с помощью unittest заключается в создании тест-кейсов для проверки функциональности отдельных частей вашего кода. Каждый тест-кейс определяется с помощью подкласса unittest.TestCase. Это позволяет наследовать несколько методов, предоставляемых классом TestCase.

Например, класс TestCase предоставляет методы assert. Эти методы позволяют проверить совпадение фактического результата операции с ожидаемым или выполнение определенных условий. Если результат не совпадает с ожидаемым, тест помечается как проваленный и вы получаете сообщение об ошибке.

Подробную информацию о различных методах, предоставляемых классом TestCase, смотрите в разделе «Классы и функции».

От редакции Pythonist: также рекомендуем почитать статью «Оператор assert в Python: объяснение на примерах».

Теперь давайте воспользуемся двумя методами assert, чтобы написать модульные тесты для простой программы-калькулятора.

Сначала создайте новую папку (каталог) с именем unit-testing. Затем создайте в этой папке файл calculator.py и скопируйте в него следующий код:

def add(x, y):
    """add numbers"""
    return x + y

def subtract(x, y):
    """subtract numbers"""
    return x - y

def divide(x, y):
    """divide numbers"""
    return x / y

def multiply(x, y):
    """multiply numbers"""
    return x * y

Обратите внимание, что вместо того, чтобы поместить вашу программу-калькулятор в одну функцию, мы разбили ее на четыре независимые функции (юниты). Это сделано для того, чтобы обеспечить независимое тестирование каждой части программы. Благодаря этому, если какой-либо из блоков выдаст ошибку во время тестирования, вы сможете легко определить этот блок и устранить неполадки, не вмешиваясь в другие части вашей программы.

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

Чтобы показать, как это работает, давайте напишем тест для функции add в программе-калькуляторе. В папке unit-testing создайте новый файл с именем test_calculator.py и скопируйте в него следующий код:

import unittest
import calculator

class TestCalculator(unittest.TestCase):
    def test_add(self):
        self.assertEqual(calculator.add(1, 2), 3)
        self.assertEqual(calculator.add(-1, 1), 0)
        self.assertEqual(calculator.add(-1, -1), -2)
        self.assertEqual(calculator.add(0, 0), 0)

В первой и второй строках вашего кода вы импортировали модули unittest и calculator. Затем вы создали класс TestCalculator, который наследуется от класса TestCase.

В пятой строке кода вы определили метод test_add в вашем классе TestCalculator. Этот метод, как и все методы экземпляра в Python, принимает self в качестве первого аргумента. Поскольку self является ссылкой на класс TestCalculator, он может получить доступ к методу assertEqual, предоставляемому классом TestCase, от которого наследуется TestCalculator.

Метод assertEqual проверяет, равны ли два значения. Он имеет следующий синтаксис:

self.assertEqual(first, second, msg=None)

Здесь first представляет значение, которое вы хотите проверить на равенство со значением second. msg — необязательный аргумент. Он представляет собой пользовательское сообщение, которое вы получите в случае неудачи проверки. Если значение msg не указано, вы получите сообщение по умолчанию.

Теперь давайте разберем использование assertEqual в методе test_add. В первом утверждении self.assertEqual(add(1, 2), 3) проверяет, равен ли результат add(1, 2) трем. Если функция возвращает 3, тест пройден. В противном случае она терпит неудачу и выводит сообщение о несоответствии.

Также обратите внимание, что в методе test_add вы тестировали только репрезентативные значения. Это гарантирует, что ваш тест охватывает широкий диапазон возможных входных данных без лишнего кода. Вот разбивка репрезентативных значений для метода test_add:

  • Сложение двух положительных чисел (self.assertEqual(calculator.add(1, 2), 3)).
  • Сложение отрицательного и положительного чисел (self.assertEqual(calculator.add(-1, 1), 0)).
  • Сложение двух отрицательных чисел (self.assertEqual(calculator.add(-1, -1), -2)).
  • Сложение двух нулей (self.assertEqual(calculator.add(0, 0), 0)).

Теперь, чтобы запустить тест, перейдите в каталог unit-testing в терминале и выполните следующую команду:

python -m unittest test_calculator.py

В терминале появится следующее сообщение:

.
----------------------------------------------------------------------
Ran 1 test in 0.000s

OK

Это сообщение указывает на то, что вы запустили один тест и он прошел успешно.

Чтобы убедиться, что тест работает так, как ожидалось, вернитесь в файл calculator.py и измените оператор сложения (+) в функции add на оператор вычитания (-):

def add(x, y):
    """add numbers"""
    return x - y

После внесения изменений при повторном запуске теста будет выдана ошибка AssertionError, показывающая, что тест провалился:

Traceback (most recent call last):
  File ".../test_calculator.py", line 6, in test_add
    self.assertEqual(calculator.add(1, 2), 3)
AssertionError: -1 != 3

----------------------------------------------------------------------
Ran 1 test in 0.000s

Возможно, вы задаетесь вопросом, почему мы при запуске теста добавляем в команду unittest, а не просто запускаем python test_calculator.py. Это потому, что вам еще предстоит сделать файл test_calculator.py самостоятельным скриптом. Поэтому пока запуск python test_calculator.py не даст вам никакого результата.

Чтобы сделать файл test_calculator.py исполняемым как отдельный скрипт, вам нужно добавить следующий код в его нижнюю часть:

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

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

Чтобы это проверить, измените название метода test_add на add_test:

class TestCalculator(unittest.TestCase):
    def add_test(self):
        self.assertEqual(calculator.add(1, 2), 3)
        self.assertEqual(calculator.add(-1, 1), 0)
        self.assertEqual(calculator.add(-1, -1), -2)
        self.assertEqual(calculator.add(0, 0), 0)

Если вы теперь выполните в терминале команду python test_calculator.py, вы получите сообщение, подобное этому:

----------------------------------------------------------------------
Ran 0 tests in 0.000s

OK

Вывод показывает, что было выполнено ноль тестов. Теперь измените имя вашего метода на test_add. Также измените оператор вычитания в функции add вашего calculator.py на оператор сложения (+). После этого повторно запустите тест командой python test_calculator.py и сравните полученный результат с предыдущим.

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

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

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

Измените файл test_calculator.py, включив в него тестовый метод для функции divide:

import unittest
import calculator

class TestMathOperations(unittest.TestCase):
    def test_add(self):
        self.assertEqual(calculator.add(1, 2), 3)
        self.assertEqual(calculator.add(-1, 1), 0)
        self.assertEqual(calculator.add(-1, -1), -2)
        self.assertEqual(calculator.add(0, 0), 0)

    def test_divide(self):
        self.assertEqual(calculator.divide(10, 2), 5)
        self.assertEqual(calculator.divide(9, 3), 3)
        self.assertEqual(calculator.divide(-6, 2), -3)
        self.assertEqual(calculator.divide(0, 1), 0)
        with self.assertRaises(ZeroDivisionError):
            calculator.divide(10, 0)

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

Ваш новый метод test_divide проверяет репрезентативные значения точно так же, как и метод test_add. Но в конце есть новый код, использующий assertRaises. assertRaises — это еще один метод assert, предоставляемый unittest для проверки, не вызывает ли ваш код исключение. Здесь вы использовали этот метод для проверки на ошибку ZeroDivisionError.

Если вы запустите тесты сейчас, то получите сообщение с двумя точками (..), указывающее на то, что вы выполнили два успешных теста:

..
----------------------------------------------------------------------
Ran 2 tests in 0.000s

OK

Заключение

Эта статья научила вас основам модульного тестирования на Python с помощью фреймворка unittest.

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

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

Перевод статьи «How to Write Unit Tests in Python – with Example Test Code».