Модульное тестирование с unittest в Python

Содержание

Что такое модульный тест?

Модульный тест в 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
Похожие посты
Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *