Введение в модульное тестирование на Python

Python

Вы только что написали фрагмент кода и не знаете, что делать дальше. Отправлять ли pull request, чтобы коллеги провели ревизию кода или же протестировать его вручную?

Конечно же, следует сделать и то, и другое, но с небольшой оговоркой: сначала нужно провести модульное тестирование и убедиться, что код работает, как надо.

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

Начинаем

Легче всего разобраться с тестированием на практических примерах. Поэтому я напишу простую функцию в файле name_function.py. Она берет имя и фамилию и возвращает полное имя:

# Генерирование отформатированного полного имени
def formatted_name(first_name, last_name):
   full_name = first_name + ' ' + last_name
   return full_name.title()

Функция formatted_name() берет имя и фамилию и соединяет их пробелом между значениями. Таким образом, генерируется полное имя. Затем первая буква каждого слова заменяется на заглавную. Для проверки работоспособности напишите код, который будет использовать данную функцию. В names.py я напишу простой код, который позволит пользователям вводить свои имя и фамилию:

from name_function import formatted_name

print("Please enter the first and last names or enter x to E[x]it.")

while True:
   first_name = input("Please enter the first name: ")
   if first_name == "x":
       print("Good bye.")
       break

   last_name = input("Please enter the last name: ")
   if last_name == "x":
       print("Good bye.")
       break

   result = formatted_name(first_name, last_name)
   print("Formatted name is: " + result + ".")

Такой код импортирует formatted_name() из name_function.py и при запуске позволяет пользователям вводить последовательности имен и фамилий. Затем он выдает отформатированные полные имена.

Модульные тесты и сценарии

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

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

Прохождение теста

Вот стандартный сценарий для написания тестов.

Cоздайте тестовый файл. Затем импортируйте модуль unittest, определите тестовый класс, который наследуется из unittest.TestCase, и, наконец, напишите цепочку методов для тестирования всех сценариев поведения функции.

Ниже приведено подробное объяснение каждой строки кода:

import unittest
from name_function import formatted_name

class NamesTestCase(unittest.TestCase):

   def test_first_last_name(self):
       result = formatted_name("pete", "seeger")
       self.assertEqual(result, "Pete Seeger")

Для начала импортируйте unittest и функцию для тестирования — formatted_name(). Затем создайте класс (например, NamesTestCase) с тестами для вашей функции formatted_name(). Этот класс наследуется из класса unittest.TestCase.

В NamesTestCase содержится один метод для проверки одной части formatted_name(). Этот метод можно назвать test_first_last_name().

Помните, что при запуске test_name_function.py будут автоматически запускаться все методы, начинающиеся с test_.

Внутри тестового метода test_first_last_name() вам нужно будет вызвать функцию для тестирования и сохранить возвращаемое значение. В нашем примере мы будем вызывать formatted_name() с аргументами pete и seeger, а результат сохраним в результирующей переменной.

В последней строке воспользуемся методом assert. Он проверяет, что полученный результат соответствует ожидаемому. Мы знаем, что функция formatted_name() будет возвращать полное имя с первыми заглавными буквами, поэтому ожидаемым результатом является Pete Seeger. Воспользуемся методом assertEqual() из unittest для проверки.

self.assertEqual(result, “Pete Seeger”)

По сути, эта строка означает следующее: сравнить значение результирующей переменной с Pete Seeger. Если значения соответствуют, — ОК, тест пройден. Если нет, то выдаем предупреждение.

Запуская test_name_function.py, вы ожидаете получить ОК, то есть подтверждение того, что тест был пройден успешно.

Ran 1 test in 0.001s

OK

Падение теста

Чтобы показать вам, как выглядит падение теста, я изменю функцию formatted_name() и добавлю туда аргумент с отчеством. Перепишем функцию следующим образом:

# #Генерирование отформатированного ФИО
def formatted_name(first_name, last_name, middle_name):
   full_name = first_name + ' ' + middle_name + ' ' + last_name
   return full_name.title()

Такая версия formatted_name() работает для пользователей, указавших отчество. Но при тестировании вы увидите, что для людей, не заполнивших это поле, функция выдаст ошибку.

И теперь при запуске функции test_name_function.py результат будет выглядеть вот так:

Error
Traceback (most recent call last):

File “test_name_function.py”, line 7, in test_first_last_name
    result = formatted_name(“pete”, “seeger”)

TypeError: formatted_name() missing 1 required positional argument: ‘middle_name’

Ran 1 test in 0.002s

FAILED (errors=1)

В этом сообщении вы видите информацию о месте непрохождения теста:

  • Первый элемент вывода — Error. Он говорит о том, что как минимум один тест в сценарии был выполнен с ошибкой.
  • Далее вам указывается файл и метод с ошибкой.
  • Затем строка с ошибкой.
  • И тип ошибки. В нашем случае — это отсутствие аргумента middle_name.
  • Вы также увидите количество выполненных тестов, время выполнения и текстовое сообщение о статусе тестирования с количеством ошибок.

Что делать при падении теста?

Прохождение теста означает, что функция работает, как нужно. Падение теста предвещает вам много «веселья».

Я встречал нескольких программистов, которые предпочитали менять сами тесты, а не дорабатывать код. Не делайте так! Потратьте время на исправление ошибки, ведь так вы лучше поймете свой код и хорошо сэкономите время в дальнейшем.

Изначально для нашей функции formatted_name() требовалось два параметра, а затем, после перезаписи, ей понадобился еще один: отчество. Добавление отчества «сломало» функцию, нарушив ее поведение. И раз уж мы решили исправлять код, а не корректировать тесты, то правильнее будет добавить необязательное поле с отчеством.

После этого нужно убедиться в прохождении тестов в обоих случаях: при указании только имени и фамилии (например, Pete Seeger), и при указании полного имени с отчеством (например, Raymond Red Reddington). Давайте еще раз изменим код formatted_name():

#Генерирование полного имени с отчеством
def formatted_name(first_name, last_name, middle_name=''):
   if len(middle_name) > 0:
       full_name = first_name + ' ' + middle_name + ' ' + last_name
   else:
       full_name = first_name + ' ' + last_name
   return full_name.title()

Теперь функция работает для имен с отчеством и без него. Еще раз убедимся, что при прохождении теста мы опять получим Pete Seeger:

Ran 1 test in 0.001s

OK

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

Добавление новых тестов

Напишите новый метод в классе NamesTestCase. Он будет тестировать отчества:

import unittest
from name_function import formatted_name

class NamesTestCase(unittest.TestCase):

    def test_first_last_name(self):
        result = formatted_name("pete", "seeger")
        self.assertEqual(result, "Pete Seeger")

    def test_first_last_middle_name(self):
        result = formatted_name("raymond", "reddington", "red")
        self.assertEqual(result, "Raymond Red Reddington")

Теперь оба теста пройдены успешно:

Ran 2 tests in 0.001s

OK

Отличная работа!

Мы написали тесты для проверки работоспособности функции с указанным или отсутствующим отчеством.

Специально для сайта ITWORLD.UZ. Новость взята с сайта NOP::Nuances of programming