Модульное тестирование с unittest в Python
- Что такое модульный тест?
- Терминология xUnit
- Пример модульного теста Python
- Запуск тестов без вызова функции unittest.main()
- Тестирование ожидаемых исключений
Что такое модульный тест?
Модульный тест в Python — это автоматизированный тест, который:
- Проверяет небольшой фрагмент кода, называемый модулем. Модуль может быть функцией или методом класса.
- Функционирует очень быстро.
- Выполняется изолированно.
Концепция модульного тестирования состоит в том, чтобы проверить каждую небольшую часть вашей программы, чтобы убедиться, что она работает правильно. Оно отличается от интеграционного тестирования, при котором проверяется совместная работа различных частей программы.
Цель модульного теста — найти ошибки. Кроме того, модульный тест может помочь провести рефакторинг существующего кода, чтобы сделать его более тестируемым и надежным.
Python предоставляет встроенный модуль unittest, который позволяет эффективно проводить модульное тестирование.
Терминология xUnit
Модуль unittest следует философии xUnit и имеет следующие основные компоненты:
- Тестируемая система — это функция, класс, метод, которые будут тестироваться.
- Класс тестового примера (unittest.TestCase): является базовым классом для всех тестовых классов. Другими словами, все тестовые классы являются подклассами класса TestCase в модуле unittest.
- Тестовые приспособления — это методы, которые выполняются до и после выполнения тестового метода.
- Assertions — это методы, которые проверяют поведение тестируемого компонента.
- Test suite — это группа связанных тестов, выполняемых вместе.
- Test runner — это программа, запускающая набор тестов.
Пример модульного теста Python
Предположим, у вас есть класс Square, который имеет свойство length и метод area(), который возвращает площадь квадрата. Класс Square находится в модуле Square.py:
class Square: def __init__(self, length) -> None: self.length = length def area(self): return self.length * self.length
Чтобы протестировать класс Square, вы создаете новый файл с именем test_square.py и импортируете модуль unittest следующим образом:
import unittest
Поскольку test_square.py должен иметь доступ к классу Square, вам необходимо импортировать его из модуля Square.py:
import unittest from square import Square
Чтобы создать тестовый пример, вы определяете новый класс под названием TestSquare, который наследуется от класса TestCase модуля unittest:
class TestSquare(unittest.TestCase): pass
Чтобы протестировать метод area(), вы добавляете метод test_area() в класс TestSquare следующим образом:
import unittest from square import Square class TestSquare(unittest.TestCase): def test_area(self): square = Square(10) area = square.area() self.assertEqual(area, 100)
В методе test_area():
- Сначала создайте новый экземпляр класса Square и инициализируйте его радиус числом 10.
- Во-вторых, вызовите метод area(), который возвращает площадь квадрата.
- В-третьих, вызовите метод AssertEqual(), чтобы проверить, равен ли результат, возвращаемый методом area(), ожидаемой площади(100).
Если площадь равна 100, метод AssertEqual() пройдет проверку. В противном случае метод AssertEqual() не пройдет тест.
Перед запуском теста вам необходимо вызвать функцию main() модуля unittest следующим образом:
import unittest from square import Square class TestSquare(unittest.TestCase): def test_area(self): square = Square(10) area = square.area() self.assertEqual(area, 100) if __name__ == '__main__': unittest.main()
Чтобы запустить тест, вы открываете терминал, переходите в папку и выполняете следующую команду:
python test_square.py
Если вы используете Linux или macOS, вам нужно вместо этого использовать команду python3:
python3 test_square.py
Он выведет следующее:
. ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
Вывод показывает, что один тест пройден, что обозначается точкой(.). Если тест не пройден, вместо точки(.) вы увидите букву F.
Чтобы получить более подробную информацию о результате теста, вы передаете аргумент verbosity со значением 2 в функцию unittest.main():
import unittest from square import Square class TestSquare(unittest.TestCase): def test_area(self): square = Square(10) area = square.area() self.assertEqual(area, 100) if __name__ == '__main__': unittest.main(verbosity=2)
Если вы запустите тест еще раз:
python test_square.py
вы получите подробную информацию:
test_area(__main__.TestSquare) ... ok ---------------------------------------------------------------------- Ran 1 test in 0.001s OK
В выходных данных указан тестовый пример с результатом «ОК» на этот раз вместо точки(.).
Запуск тестов без вызова функции unittest.main()
- Сначала удалите блок if, который вызывает функцию unittest.main():
import unittest from square import Square class TestSquare(unittest.TestCase): def test_area(self): square = Square(10) area = square.area() self.assertEqual(area, 100)
- Во-вторых, выполните следующую команду для запуска теста:
python3 -m unittest
Эта команда обнаруживает все тестовые классы, имена которых начинаются с Test*, расположенные в файле test_*, и выполняет методы тестирования, начинающиеся с test*. опция -m обозначает модуль.
В этом примере команда выполняет метод test_area() класса TestSquare в тестовом модуле test_square.py.
Если вы используете macOS или Linux, вам нужно вместо этого использовать команду python3:
python3 -m unittest
Он вернет что-то вроде:
. ---------------------------------------------------------------------- Ran 1 test in 0.000s OK
Чтобы отобразить дополнительную информацию, вы можете добавить опцию -v к приведенной выше команде. v означает многословие. Это похоже на вызов unittest.main() с аргументом verbosity со значением 2.
python -m unittest -v
Выход:
test_area(test_square.TestSquare) ... ok ---------------------------------------------------------------------- Ran 1 tests in 0.000s OK
Тестирование ожидаемых исключений
Конструктор Square принимает параметр length. Параметр length должен быть целым или плавающим. Если вы передадите значение, которого нет в этих типах, конструктор Square должен вызвать исключение TypeError.
Чтобы проверить, вызывает ли конструктор Square исключение TypeError, вы используете метод AssertRaises() в диспетчере контекста следующим образом:
import unittest from square import Square class TestSquare(unittest.TestCase): def test_area(self): square = Square(10) area = square.area() self.assertEqual(area, 100) def test_length_with_wrong_type(self): with self.assertRaises(TypeError): square = Square('10')
Если вы запустите тест еще раз, он завершится неудачно:
python -m unittest -v
Выход:
test_area(test_square.TestSquare) ... ok test_length_with_wrong_type(test_square.TestSquare) ... FAIL ====================================================================== FAIL: test_length_with_wrong_type(test_square.TestSquare) ---------------------------------------------------------------------- Traceback(most recent call last): File "D:\python-unit-testing\test_square.py", line 13, in test_length_with_wrong_type with self.assertRaises(TypeError): AssertionError: TypeError not raised ---------------------------------------------------------------------- Ran 2 tests in 0.001s
Метод test_length_with_wrong_type() ожидал, что конструктор Square вызовет исключение TypeError. Однако этого не произошло.
Чтобы пройти тест, вам нужно вызвать исключение, если тип свойства длины не int или float в конструкторе Square:
class Square: def __init__(self, length) -> None: if type(length) not in [int, float]: raise TypeError('Length must be an integer or float') self.length = length def area(self): return self.length * self.length
Теперь все тесты пройдены:
python -m unittest -v
Выход:
test_area(test_square.TestSquare) ... ok test_length_with_wrong_type(test_square.TestSquare) ... ok ---------------------------------------------------------------------- Ran 2 tests in 0.001s OK
В следующем примере добавляется тест, который ожидает исключение ValueError, если длина равна нулю или отрицательна:
import unittest from square import Square class TestSquare(unittest.TestCase): def test_area(self): square = Square(10) area = square.area() self.assertEqual(area, 100) def test_length_with_wrong_type(self): with self.assertRaises(TypeError): square = Square('10') def test_length_with_zero_or_negative(self): with self.assertRaises(ValueError): square = Square(0) square = Square(-1)
Если вы запустите тест, он завершится неудачно:
python -m unittest -v
Выход:
test_area(test_square.TestSquare) ... ok test_length_with_wrong_type(test_square.TestSquare) ... ok test_length_with_zero_or_negative(test_square.TestSquare) ... FAIL ====================================================================== FAIL: test_length_with_zero_or_negative(test_square.TestSquare) ---------------------------------------------------------------------- Traceback(most recent call last): File "D:\python-unit-testing\test_square.py", line 17, in test_length_with_zero_or_negative with self.assertRaises(ValueError): AssertionError: ValueError not raised ---------------------------------------------------------------------- Ran 3 tests in 0.001s FAILED(failures=1)
Чтобы тест прошел, вы добавляете еще одну проверку в конструктор Square():
class Square: def __init__(self, length) -> None: if type(length) not in [int, float]: raise TypeError('Length must be an integer or float') if length < 0: raise ValueError('Length must not be negative') self.length = length def area(self): return self.length * self.length
Теперь все три теста пройдены:
python -m unittest -v
Выход:
test_area(test_square.TestSquare) ... ok test_length_with_wrong_type(test_square.TestSquare) ... ok test_length_with_zero_or_negative(test_square.TestSquare) ... ok ---------------------------------------------------------------------- Ran 3 tests in 0.001s OK