В первой части этой главы мы писали тесты для отдельной функции. Сейчас мы займемся написанием тестов для класса. Классы будут использоваться во многих ваших программах, поэтому возможность доказать, что ваши классы работают правильно, будет безусловно полезной. Если тесты для класса, над которым вы работаете, проходят успешно, вы можете быть уверены в том, что усовершенствования класса не приведут к случайному нарушению его текущего поведения.
Разные методы assert
Класс unittest.TestCase содержит целое семейство проверочных методов assert. Как упоминалось ранее, эти методы проверяют, выполняется ли условие, которое должно выполняться в определенной точке вашего кода. Если условие истинно, как и предполагалось, то ваши ожидания относительно поведения части вашей программы подтверждаются; вы можете быть уверены в отсутствии ошибок. Если же условие, которое должно быть истинным, окажется ложным, то Python выдает исключение.
В табл. 11.1 перечислены шесть часто используемых методов assert. С их помощью можно проверить, что возвращаемые значения равны или не равны ожидаемым, что значения равны True или False или что значения входят или не входят в заданный список. Эти методы могут использоваться только в классах, наследующих от unittest.TestCase; рассмотрим пример использования такого метода в контексте тестирования реального класса.
Таблица 11.1. Методы assert, предоставляемые модулем unittest
Метод | Использование |
assertEqual(a, b) | Проверяет, что a == b |
assertNotEqual(a, b) | Проверяет, что a != b |
assertTrue(x) | Проверяет, что значение x истинно |
assertFalse(x) | Проверяет, что значение x ложно |
assertIn(элемент, список) | Проверяет, что элемент входит в список |
assertNotIn(элемент, список) | Проверяет, что элемент не входит в список |
Класс для тестирования
Тестирование класса имеет много общего с тестированием функции — значительная часть работы направлена на тестирование поведения методов класса. Впрочем, существуют и различия, поэтому мы напишем отдельный класс для тестирования. Возьмем класс для управления проведением анонимных опросов:
survey.py
class AnonymousSurvey():
. ."""Сбор анонимных ответов на опросы."""
. .
(1) . .def __init__(self, question):
. . . ."""Сохраняет вопрос и готовится к сохранению ответов."""
. . . .self.question = question
. . . .self.responses = []
. . . .
(2) . .def show_question(self):
. . . ."""Выводит вопрос."""
. . . .print(question)
. . . .
(3) . .def store_response(self, new_response):
. . . ."""Сохраняет один ответ на опрос."""
. . . .self.responses.append(new_response)
. . . .
(4) . .def show_results(self):
. . . ."""Выводит все полученные ответы."""
. . . .print("Survey results:")
. . . .for response in responses:
. . . . . .print('- ' + response)
Класс начинается с вопроса, предоставленного администратором (1) , и включает пустой список для хранения ответов. Класс содержит методы для вывода вопроса (2), добавления нового ответа в список ответов (3) и вывода всех ответов, хранящихся в списке (4). Чтобы создать экземпляр на основе этого класса, необходимо предоставить вопрос. После того как будет создан экземпляр, представляющий конкретный опрос, программа выводит вопрос методом show_question(), сохраняет ответ методом store_response() и выводит результаты вызовом show_results().
Чтобы продемонстрировать, что класс AnonymousSurvey работает, напишем программу, которая использует этот класс:
language_survey.py
from survey import AnonymousSurvey
# Определение вопроса с созданием экземпляра AnonymousSurvey.
question = "What language did you first learn to speak?"
my_survey = AnonymousSurvey(question)
# Вывод вопроса и сохранение ответов.
my_survey.show_question()
print("Enter 'q' at any time to quit.\n")
while True:
. .response = input("Language: ")
. .if response == 'q':
. . . .break
. .my_survey.store_response(response)
# Вывод результатов опроса.
print("\nThank you to everyone who participated in the survey!")
my_survey.show_results()
Программа определяет вопрос и создает объект AnonymousSurvey на базе этого вопроса. Программа вызывает метод show_question() для вывода вопроса, после чего переходит к получению ответов. Каждый ответ сохраняется сразу же при получении. Когда ввод ответов был завершен (пользователь ввел q), метод show_results() выводит результаты опроса:
What language did you first learn to speak?
Enter 'q' at any time to quit.
Language: English
Language: Spanish
Language: English
Language: Mandarin
Language: q
Thank you to everyone who participated in the survey!
Survey results:
- English
- Spanish
- English
- Mandarin
Этот класс работает для простого анонимного опроса. Но допустим, вы решили усовершенствовать класс AnonymousSurvey и модуль survey, в котором он находится. Например, каждому пользователю будет разрешено ввести несколько ответов. Или вы напишете метод, который будет выводить только уникальные ответы и сообщать, сколько раз был дан тот или иной ответ. Или вы напишете другой класс для проведения неанонимных опросов.
Реализация таких изменений грозит повлиять на текущее поведение класса AnonymousSurvey. Например, может оказаться, что поддержка ввода нескольких ответов случайно повлияет на процесс обработки одиночных ответов. Чтобы гарантировать, что доработка модуля не нарушит существующего поведения, для класса нужно написать тесты.
Тестирование класса AnonymousSurvey
Напишем тест, проверяющий всего один аспект поведения AnonymousSurvey. Этот тест будет проверять, что один ответ на опрос сохраняется правильно. После того как метод будет сохранен, метод assertIn() проверяет, что он действительно находится в списке ответов:
test_survey.py
import unittest
from survey import AnonymousSurvey
(1) class TestAnonmyousSurvey(unittest.TestCase):
. ."""Тесты для класса AnonymousSurvey"""
. .
(2) . .def test_store_single_response(self):
. . . ."""Проверяет, что один ответ сохранен правильно."""
. . . .question = "What language did you first learn to speak?"
(3) . . . .my_survey = AnonymousSurvey(question)
. . . .my_survey.store_response('English')
. . . .
(4) . . . .self.assertIn('English', my_survey.responses)
unittest.main()
Программа начинается с импортирования модуля unittest и тестируемого класса AnonymousSurvey. Тестовый сценарий TestAnonymousSurvey, как и в предыдущих случаях, наследует от unittest.TestCase (1) . Первый тестовый метод проверяет, что сохраненный ответ действительно попадает в список ответов опроса. Этому методу присваивается хорошее содержательное имя test_store_single_response() (2). Если тест не проходит, имя метода в выходных данных сбойного теста ясно показывает, что проблема связана с сохранением отдельного ответа на опрос.
Чтобы протестировать поведение класса, необходимо создать экземпляр класса. В точке (3) создается экземпляр с именем my_survey для вопроса "What language did you first learn to speak?", Один ответ (English) сохраняется с использованием метода store_response(). Затем программа убеждается в том, что ответ был сохранен правильно; для этого она проверяет, что значение English присутствует в списке my_survey.responses (4).
При запуске программы test_survey.py тест проходит успешно:
.
----------------------------------------------------------------------
Ran 1 test in 0.001s
OK
Неплохо, но опрос с одним ответом вряд ли можно назвать полезным. Убедимся в том, что три ответа сохраняются правильно. Для этого в TestAnonymousSurvey добавляется еще один метод:
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
"""Тесты для класса AnonymousSurvey"""
def test_store_single_response(self):
"""Проверяет, что один ответ сохранен правильно."""
...
. . . .
. .def test_store_three_responses(self):
. . . ."""Проверяет, что три ответа были сохранены правильно."""
. . . .question = "What language did you first learn to speak?"
. . . .my_survey = AnonymousSurvey(question)
(1) . . . .responses = ['English', 'Spanish', 'Mandarin']
. . . .for response in responses:
. . . . . .my_survey.store_response(response)
. . . . . .
(2) . . . .for response in responses:
. . . . . .self.assertIn(response, my_survey.responses)
unittest.main()
Новому методу присваивается имя test_store_three_responses(). Мы создаем объект опроса по аналогии с тем, как это делалось в test_store_single_response(). Затем определяется список, содержащий три разных ответа (1) , и для каждого из этих ответов вызывается метод store_response(). После того как ответы будут сохранены, следующий цикл проверяет, что каждый ответ теперь присутствует в my_survey.responses (2).
Если снова запустить test_survey.py, оба теста (для одного ответа и для трех ответов) проходят успешно:
..
----------------------------------------------------------------------
Ran 2 tests in 0.000s
OK
Все прекрасно работает. Тем не менее тесты выглядят немного однообразно, поэтому мы воспользуемся еще одной возможностью unittest для повышения их эффективности.
Метод setUp()
В программе test_survey.py в каждом тестовом методе создавался новый экземпляр AnonymousSurvey, а также новые ответы. Класс unittest.TestCase содержит метод setUp(), который позволяет создать эти объекты один раз, а затем использовать их в каждом из тестовых методов. Если в класс TestCase включается метод setUp(), Python выполняет метод setUp() перед запуском каждого метода, имя которого начинается с test_. Все объекты, созданные методом setUp(), становятся доступными во всех написанных вами тестовых методах.
Используем setUp() для создания экземпляра AnonymousSurvey и набора ответов, которые могут использоваться в test_store_single_response() и test_store_three_responses():
import unittest
from survey import AnonymousSurvey
class TestAnonymousSurvey(unittest.TestCase):
"""Тесты для класса AnonymousSurvey."""
. .
. .def setUp(self):
. . . ."""
. . . .Создание опроса и набора ответов для всех тестовых методов.
. . . ."""
. . . .question = "What language did you first learn to speak?"
(1) . . . .self.my_survey = AnonymousSurvey(question)
(2) . . . .self.responses = ['English', 'Spanish', 'Mandarin']
def test_store_single_response(self):
"""Проверяет, что один ответ сохранен правильно."""
. . . .self.my_survey.store_response(self.responses[0])
. . . .self.assertIn(self.responses[0], self.my_survey.responses)
. . . .
def test_store_three_responses(self):
"""Проверяет, что три ответа были сохранены правильно."""
. . . .for response in self.responses:
. . . . . .self.my_survey.store_response(response)
. . . .for response in self.responses:
. . . . . .self.assertIn(response, self.my_survey.responses)
unittest.main()
Метод setUp() решает две задачи: он создает экземпляр опроса (1) и список ответов (2). Каждый из этих атрибутов снабжается префиксом self, поэтому он может использоваться где угодно в классе. Это обстоятельство упрощает два тестовых метода, потому что им уже не нужно создавать экземпляр опроса или ответы. Метод test_store_single_response() убеждается в том, что первый ответ в self.responses — self.responses[0] — сохранен правильно, а метод test_store_single_response() убеждается в том, что правильно были сохранены все три ответа в self.responses.
При повторном запуске test_survey.py оба теста по-прежнему проходят. Эти тесты будут особенно полезными при расширении AnonymousSurvey с поддержкой нескольких ответов для каждого участника. После внесения изменений вы можете повторить тесты и убедиться в том, что изменения не повлияли на возможность сохранения отдельного ответа или серии ответов.
При тестировании классов, написанных вами, метод setUp() упрощает написание тестовых методов. Вы создаете один набор экземпляров и атрибутов в setUp(), а затем используете эти экземпляры во всех тестовых методах. Это намного проще и удобнее, чем создавать новый набор экземпляров и атрибутов в каждом тестовом методе.
примечание
Во время работы тестового сценария Python выводит один символ для каждого модульного теста после его завершения. Для прошедшего теста выводится точка; если при выполнении произошла ошибка, выводится символ E, а если не прошла проверка условия assert, выводится символ F. Вот почему вы увидите другое количество точек и символов в первой строке вывода при выполнении ваших тестовых сценариев. Если выполнение тестового сценария занимает слишком много времени, потому что сценарий содержит слишком много тестов, эти символы дадут некоторое представление о количестве прошедших тестов.
Упражнения
11-3. Работник: напишите класс Employee, представляющий работника. Метод __init__() должен получать имя, фамилию и ежегодный оклад; все эти значения должны сохраняться в атрибутах. Напишите метод give_raise(), который по умолчанию увеличивает ежегодный оклад на $5000 — но при этом может получать другую величину прибавки.
Напишите тестовый сценарий для Employee. Напишите два тестовых метода, test_give_default_raise() и test_give_custom_raise(). Используйте метод setUp(), чтобы вам не приходилось заново создавать экземпляр Employee в каждом тестовом методе. Запустите свой тестовый сценарий и убедитесь в том, что оба теста прошли успешно.