
Модульное тестирование, тестирование отдельных модулей программы, является стандартной отправной точкой для тестирования программы. Проверка того, что модули программы работают изолированно друг от друга, прежде чем пытаться интеграционное тестирование — тестирование этих модулей в комбинации — упрощает общее тестирование работы программы.
В этой статье представлены две стандартные среды Python для модульного тестирования:
docttest— классический фреймворк, использующий специально отформатированные комментарии, встроенные в исходный код, для проведения тестирования.unittest— более поздний фреймворк, порт кросс-языковогоnUnitтестового фреймворка на Python, который использует классы со специально названными методами для управления тестированием.
Доктест
Строки документации имеют особое значение для модуля doctest библиотеки Python. Метод run этого модуля
- ищет в строках документации подстроки, отформатированные как тестовые примеры, подобные строкам документации
- Тестовые случаи сигнализируются начальными строками подсказок «›››».
- выполняет эти команды, чтобы подтвердить, что они возвращают указанные результаты и/или имеют желаемые эффекты.
- Ожидаемые результаты приведены после выполнения команд.
- Результаты «Traceback» обрабатываются особым образом: … с включенной опцией ELLIPSIS заставляет
doctestигнорировать детали Traceback при проверке ожидаемых результатов.
В документации по библиотеке Python приведены дополнительные примеры запуска doctest для целых модулей, т. е. файлов .py, из командной строки, а также из кодов Python: способ использования, который, как говорится в руководстве, гораздо более распространен на практике. Давайте взглянем на пример кода для генерации степеней двойки с кодом doctest.
def powers_of_2():
"""generate successive powers of 2, starting with 2^0 """
current_power_of_2 = 1
while True:
yield current_power_of_2
current_power_of_2 *= 2
def print_first_n_powers_of_2(n):
""" print the first n powers of 2, starting with 2^0
routine prints the first n powers of 2 for a user-supplied integer n,
starting with 2^0 and ending with 2^n-1.
>>> print_first_n_powers_of_2(0.5)
Traceback (most recent call last):
...
TypeError: 'float' object cannot be interpreted as an integer
>>> print_first_n_powers_of_2(-1)
>>> print_first_n_powers_of_2(0)
>>> print_first_n_powers_of_2(1)
1
>>> print_first_n_powers_of_2(5)
1
2
4
8
16
"""
p2 = powers_of_2() # obtain a copy of the generator function for local use
for i in range(0,n): print(next(p2))
import doctest
# Show all test cases and their results as the cases execute
doctest.run_docstring_examples(print_first_n_powers_of_2, None, optionflags=doctest.ELLIPSIS, verbose=True)
# Limit output to failed tests (default)
doctest.run_docstring_examples(print_first_n_powers_of_2, None, optionflags=doctest.ELLIPSIS)
Модульный тест
Стандартная библиотека Python включает unittest: платформу для автоматизации тестирования программ Python. unittest управляет тестированием с помощью трех основных компонентов:
- Тестовый пример — код, который проверяет операции второго кода. Тестовый пример должен проверять один аспект тестируемого кода.
- Набор тестов – набор тестовых наборов или других наборов тестов, которые необходимо тестировать вместе. В то время как
unittestбудет автоматически комбинировать тестовые наборы,TestSuiteобеспечивает больший контроль над тем, что тестирует сеанс тестирования. - Test Runner — часть
unittest, управляющая выполнением и выводом теста. Он собирает данные об успехах и неудачах и выводит сводную информацию.
unittest использует значения, возвращаемые unittest методами assert, для определения результата теста. Средство выполнения тестов собирает эти значения и отображает результаты в конце теста. Наиболее распространенные методы assert включают следующее:
assertEqual(a, b) — утверждает a == bassertNotEqual(a, b) — утверждает a != bassertTrue(a) — утверждает a is TrueassertFalse(a) — утверждает a is falseassertIn(a, b) — утверждает a является членом bassertNotIn(a) — утверждает, что a не является членом b
Полный список методов assert см. в unittest документации.
Создание и запуск тестовых случаев
Тестовый пример unittest — это метод со специальным именем в подклассе класса TestCase класса unittest. Все методы тестирования должны начинаться со слова «тест».
Методы тестирования должны использовать один или несколько методов утверждения, которые проверяют работу кода. После завершения каждого тестового примера средство выполнения тестов отображает сводку значений, возвращаемых его методами утверждения. Неудачные тесты будут отображать трассировку неудачного метода утверждения и описательное сообщение об ошибке.
В приведенном ниже коде показано создание и выполнение тестового примера с использованием методов утверждения для проверки методов классов формы. Выходные данные включают данные пройденного и не пройденного теста.
import unittest
import math
class Shape:
def __init__(self, name, color):
self.name = name
self.color = color
def describe(self):
return f"Name: {self.name}\nColor: {self.color}"
class Square(Shape):
def __init__(self, name, color, side):
super().__init__(name, color)
self.side = side
def get_area(self):
return self.side * self.side
def get_perimeter(self):
return self.side * 4
class Circle(Shape):
def __init__(self, name, color, radius):
super().__init__(name, color)
self.radius = radius
def get_area(self):
return math.pi * (self.radius * self.radius)
# Must inherit from TestCase
class TestShapes(unittest.TestCase):
# Test cases all begin with word 'test'
def test_constructors(self):
# Test the Shape class
my_shape = Shape("shape", "blue")
self.assertEqual(my_shape.name, "shape") # Assert both arguments are equal
self.assertEqual(my_shape.color, "blue")
# Test the Square subclass
my_square = Square("square", "red", 8)
self.assertEqual(my_square.name, "square")
self.assertEqual(my_square.color, "red")
self.assertEqual(my_square.side, 8)
# Test the Circle subclass
my_circle = Circle("circle", "yellow", 4)
self.assertEqual(my_circle.name, "circle")
self.assertEqual(my_circle.color, "yellow")
self.assertEqual(my_circle.radius, 4)
def test_square_area(self):
my_square = Square("square", "red", 8)
self.assertTrue(my_square.get_area() == 63) # This test will fail
def test_circle_area(self):
my_circle = Circle("circle", "yellow", 4)
self.assertTrue(my_circle.get_area() == math.pi * (4 * 4)) # Assert expression evaluates True
# Use the main function from the unittest module
unittest.main(argv=[''], verbosity=2, exit=False)
Организация и расширение модульного тестирования
unittest предоставляет дополнительные методы для инициализации и завершения тестирования:
setUp()— если этот метод присутствует, средство запуска тестов выполнит его перед любым другим методом, что сделает его местом для инстанцирования объектов, создания или открытия файлов или инициализации других ресурсов, которые будут использовать методы тестирования.tearDown()— если этот метод присутствует, средство запуска тестов выполнит его последним, что сделает его местом для финализации и освобождения ресурсов после выполнения тестов.
unittest также предоставляет три декоратора, которые определяют условия, при которых тесты могут быть пропущены:
@unittest.skip(reason)- безоговорочно пропустить этот тест@unittest.skipIf(condition, reason)— пропустить этот тест, когдаconditionоценивается как True@unittest.skipUnless(reason)— пропустить этот тест, когдаconditionоценивается как False
unittest отображает аргумент reason, когда тест пропускается.
import unittest
class Letters:
def __init__(self, letters):
self.letters = letters
def compare_letters(self, a, b):
# Return 1 is a > b, -1 if a < b, 0 if a == b
return 1 if ord(a) > ord(b) else (-1 if ord(a) < ord(b) else 0)
def get_dict(self):
# Return dict with keys of letters in list and values of how many times they appear
d = {}
for letter in self.letters:
if letter in d.keys():
d[letter] += 1
else:
d[letter] = 1
return d
def combine_letters(self):
# Combine letters in list to make a string
string = ""
for letter in self.letters:
string += letter
return string
class TestShapes(unittest.TestCase):
def setUp(self):
# Create objects that the test methods will use
self.my_letters1 = Letters( ['a', 'b', 'c', 'c', 'd', 'd'] )
self.my_letters2 = Letters( ['a', 'a', 'b', 'b', 'c', 'd'] )
self.my_letters3 = Letters( ['p', 'r', 'o', 'g', 'r', 'a', 'm', 'm', 'i', 'n', 'g'] )
@unittest.skipIf('get_dict' not in dir(Letters), "get_dict does not exist in class Letters")
def test_get_dict(self):
my_dict1 = self.my_letters1.get_dict()
my_dict2 = self.my_letters2.get_dict()
self.assertIn( "d", my_dict1.keys() )
self.assertEqual( my_dict2['b'], 2)
@unittest.skip("unconditional skip") # This test will always be skipped
def test_combine_letters(self):
string = self.my_letters3.combine_letters()
self.assertEqual( string, "programming")
@unittest.expectedFailure # This test will 'pass' if assert method fails
def test_compare_letters(self):
result = self.my_letters1.compare_letters(5, 5) # Should return 0
self.assertEqual( result, 1 ) # This will fail, but expected failure
def tearDown(self):
# Free up object memory
del self.my_letters1
del self.my_letters2
del self.my_letters3
# Use the main function from the unittest module
unittest.main(argv=[''], verbosity=2, exit=False)