19. Сервируем стол (метод setUp)
Начав писать тесты, вы обнаружите, что действуете в рамках некоторой общей последовательности (Билл Уэйк (Bill Wake) придумал сокращение 3A – Arrange, Act, Assert):
• вначале вы создаете некоторые тестовые объекты – Arrange;
• затем заставляете эти объекты действовать – Act;
• потом проверяете результаты их работы – Assert.
Вызов тестового метода
Вызов метода setUp перед обращением к методу
Вызов метода tearDown после обращения к методу
Метод tearDown должен вызываться даже в случае неудачи теста
Выполнение нескольких тестов
Отчет о результатах
Первый этап – Arrange – зачастую совпадает для нескольких разных тестов, в то время как второй и третий этапы для разных тестов различаются. У меня есть два числа: 7 и 9. Если я сложу их, я должен получить 16; если я вычту второе из первого, я ожидаю получить –2; наконец, если я перемножу их, я полагаю, должно получиться 63. Операции и ожидаемые результаты различаются, однако исходные данные одни и те же – два числа: 7 и 9.
Если подобное повторяется раз за разом в разных масштабах (а ведь так оно и есть), мы должны подумать о том, как можно оптимизировать создание тестовых объектов. Возникает конфликт между следующими двумя ограничениями:
Производительность. Мы хотим, чтобы тесты срабатывали как можно быстрее. Отсюда следует, что если одни и те же объекты используются в нескольких тестах, желательно, чтобы создание этих объектов выполнялось всего один раз.
Изоляция. Успех или неудача одного теста никак не должны влиять на работу других тестов. Если несколько тестов используют одни и те же объекты и если один из тестов меняет внутреннее состояние какого-либо объекта, результаты работы остальных тестов, скорее всего, изменятся.
Взаимозависимость между тестами приводит к одному весьма неприятному эффекту: если один тест перестает работать, остальные десять тестов тоже перестают работать, несмотря на то, что тестируемый ими код выполняется правильно. Характерен также и другой, еще более неприятный эффект, когда порядок выполнения тестов имеет значение: если я запускаю тест A перед тестом Б, оба теста выполняются успешно, если я запускаю тест Б перед тестом А, тест А не выполняется. Или, еще хуже, код, проверяемый тестом Б, действует неправильно, однако из-за того, что тест А запускается перед тестом Б, тест Б выполняется успешно.
Итак, мы хотим избежать взаимозависимости между тестами. Предположим, что мы можем сделать процедуру создания объектов достаточно быстрой. В этом случае мы могли бы создавать объекты для теста каждый раз перед выполнением очередного теста. Этот подход в замаскированном виде уже использовался нами в классе WasRun, в котором требовалось, чтобы перед запуском теста флаг wasRun сбрасывался в состояние «ложь». Напишем тест:
TestCaseTest
def testSetUp(self):
test= WasRun("testMethod")
test.run()
assert(test.wasSetUp)
Чтобы запустить этот код, необходимо добавить в конец нашего файла строку TestCaseTest(«testSetUp»). run(). Интерпретатор вежливо сообщает нам, что атрибут с именем wasSetUp отсутствует. И немудрено, ведь мы пока еще не определили значение этого атрибута. Вот необходимый для этого код:
WasRun
def setUp(self):
self.wasSetUp= 1
Однако метод setUp() должен быть откуда-то вызван. Обращение к методу setUp() – это работа класса TestCase. Добавим соответствующий код:
TestCase
def setUp(self):
pass
def run(self):
self.setUp()
method = getattr(self, self.name)
method()
Чтобы заставить тест работать, мы сделали целых два шага – это слишком много, особенно если учесть, что мы движемся почти вслепую. Проверим, работает ли тест? Да, работает. Однако если вы хотите чему-то научиться, попробуйте придумать, как мы можем заставить тест работать, изменяя лишь по одному методу за один шаг.
Немедленно воспользуемся новым механизмом, чтобы сократить длину наших тестов. Прежде всего упростим класс WasRun, для этого перенесем процедуру установки флага wasRun в метод setUp():
WasRun
def setUp(self):
self.wasRun = None
self.wasSetUp = 1
Теперь можно упростить метод testRunning() – освободить его от
обязанности проверять состояние флага перед вызовом тестового метода. Можем ли мы быть настолько уверенными в правильной работе нашего кода? Только при условии, что в наборе тестов присутствует тестовый метод testSetUp(). Это часто встречающийся шаблон – один тест может быть простым, только если в системе имеется другой тест, выполняющийся успешно:
TestCaseTest
def testRunning(self):
test = WasRun("testMethod")
test.run()
assert(test.wasRun)
Мы также можем упростить сами тесты. В обоих случаях мы создаем экземпляр класса WasRun, а ведь задача создания тестовых объектов возлагается на подготовительный этап – именно об этом мы с вами говорили. Стало быть, мы можем создать объект WasRun в методе setUp(), а затем использовать его в тестовых методах. Каждый тестовый метод выполняется в отдельном экземпляре класса TestCaseTest, поэтому два разных теста не могут быть взаимозависимы. (Мы исходим из того, что объект не будет взаимодействовать с внешним миром некоторым непредусмотренным уродливым способом, например путем изменения значений глобальных переменных.)
TestCaseTest
def setUp(self):
self.test = WasRun("testMethod")
def testRunning(self):
self.test.run()
assert(self.test.wasRun)
def testSetUp(self):
self.test.run()
assert(self.test.wasSetUp)
Вызов тестового метода
Вызов метода setUp перед обращением к методу
Вызов метода tearDown после обращения к методу
Метод tearDown должен вызываться даже в случае неудачи теста
Выполнение нескольких тестов
Отчет о результатах
Теперь сделаем так, чтобы после выполнения тестового метода обязательно выполнялся метод tearDown().
В данной главе мы
• решили, что на текущий момент тестов важнее простота, чем их производительность;
• написали тест для метода setUp() и реализовали этот метод;
• использовали метод setUp(), чтобы упростить тестируемый объект-контейнер теста;
• использовали метод setUp(), чтобы упростить тесты, проверяющие созданный нами тестовый объект (я же говорил, что временами это напоминает нейрохирургическую операцию на собственном мозге).