18. Первые шаги на пути к xUnit
Разработка инструмента тестирования с использованием самого этого инструмента для тестирования многим может показаться чем-то, напоминающим хирургическую операцию на своем собственном мозге. («Только не вздумай трогать центры моторики! О! Слишком поздно! Игра окончена».) Сначала эта идея может показаться жутковатой. Однако инфраструктура тестирования обладает более сложной внутренней логикой, если сравнивать с относительно несложным денежным примером, рассмотренным в первой части книги. Часть II можно рассматривать как шаг в сторону разработки «настоящего» программного обеспечения. Кроме того, вы можете рассматривать этот материал как упражнение в самодокументируемом программировании.
Прежде всего, у нас должна быть возможность создать тест и запустить тестовый метод. Например: TestCase("testMethod"). run(). Возникает проблема: мы собираемся написать тест для программного кода, который мы будем использовать для написания тестов. Так как у нас пока еще нет даже намека на инфраструктуру тестирования, мы вынуждены проверить правильность нашего самого первого шага вручную. К счастью, мы достаточно хорошо отдохнули, а значит, вероятность того, что мы допустим ошибку, относительно невелика. Однако чтобы сделать ее еще меньше, мы планируем двигаться маленькими-маленькими шажками, тщательно проверяя все, что мы делаем. Вот список задач, который приходит на ум, когда начинаешь размышлять о разработке собственной инфраструктуры тестирования:
Вызов тестового метода
Вызов метода setUp перед обращением к методу
Вызов метода tearDown после обращения к методу
Метод tearDown должен вызываться даже в случае неудачи теста
Выполнение нескольких тестов
Отчет о результатах
Конечно же, мы по-прежнему работаем в стиле «сначала тесты». Для нашего прототеста нам потребуется небольшая программа, которая должна отображать на экране значение «истина», если произошло обращение к тестовому методу, и значение «ложь» в противном случае. Теперь представим, что у нас есть тест, который устанавливает флаг внутри тестового метода, в этом случае мы могли бы после выполнения теста отобразить состояние флага на экране и самостоятельно убедиться в том, что флаг установлен правильно. Выполнив проверку вручную, мы сможем попробовать автоматизировать процесс.
Итак, у нас наметилась следующая стратегия. Мы создаем объект, который соответствует нашему тесту. В объекте содержится флаг. Перед выполнением тестового метода флаг должен быть установлен в состояние «ложь». Тестовый метод устанавливает флаг в состояние «истина». После выполнения тестового метода мы должны проверить состояние флага. Назовем наш тестовый класс именем WasRun, так как объект этого класса будет сигнализировать нам о том, был ли выполнен тестовый метод. Флаг внутри этого класса также будет называться wasRun (это несколько сбивает с толку, однако wasRun – такое подходящее имя). Собственно объект (экземпляр класса WasRun) будет называться просто test. То есть мы сможем написать инструкцию assert test.wasRun (assert – встроенная инструкция языка Python).
Язык программирования Python является интерпретируемым – команды исполняются по мере чтения их из файла с исходным кодом. Поэтому, чтобы выполнить тестирование, мы можем написать следующий короткий файл и попробовать запустить его:
test = WasRun(«testMethod»)
print(test.wasRun)
test.testMethod()
print(test.wasRun)
Мы ожидаем, что эта миниатюрная программа напечатает None до выполнения тестового метода и 1 – после. (В языке Python значение None является аналогом null или nil и наряду с числом 0 соответствует значению «ложь».) Однако программа не делает того, что мы от нее ждем. И немудрено – мы еще не определили класс WasRun (сначала тесты!).
WasRun
class WasRun:
pass
(Ключевое слово pass используется в случае, если реализация класса или метода отсутствует.) Теперь интерпретатор сообщает нам, что в классе WasRun нет атрибута с именем wasRun. Создание атрибута происходит в момент создания объекта (экземпляра класса), то есть в процессе выполнения конструктора (для удобства конструктор любого класса называется __init__). Внутри конструктора мы присваиваем флагу wasRun значение None (ложь):
WasRun
class WasRun:
def __init__(self, name):
self.wasRun = None
Теперь программа действительно отображает на экране значение None, однако после этого интерпретатор сообщает нам, что мы должны определить в классе WasRun метод testMethod. (Было бы неплохо, если бы среда разработки автоматически реагировала на это: самостоятельно создавала бы функцию-заглушку и открывала редактор с курсором, установленным в теле этой функции. Не правда ли, это было бы просто здорово? Кстати, некоторые производители IDE уже додумались до этого.)
WasRun
def testMethod(self):
pass
Запускаем файл и видим на экране два значения: None и None. Нам хотелось бы видеть None и 1. Чтобы получить желаемый результат, в теле метода testMethod присвоим флагу wasRun желаемое значение:
WasRun
def testMethod(self):
self.wasRun = 1
Запускаем программу – то, что нужно! Мы получили желаемый результат. Зеленая полоса – ур-р-ра! Нам предстоит сложный рефакторинг, однако если мы видим перед собой зеленую полосу, значит, мы добились прогресса.
Теперь, вместо того чтобы напрямую обращаться к нашему тестовому методу, мы должны использовать наш реальный интерфейс – метод run(). Изменим тест следующим образом:
test= WasRun(«testMethod»)
print(test.wasRun)
test.run()
print(test.wasRun)
Чтобы заставить тест работать, достаточно воспользоваться следующей несложной реализацией:
WasRun
def run(self):
self.testMethod()
Наша тестовая программа снова печатает на экране то, что нам нужно. Зачастую во время рефакторинга возникает ощущение, что необходимо разделить код, с которым вы работаете, на две части, чтобы работать с ними по отдельности. Если в конце работы они снова сольются воедино, – замечательно. Если нет, значит, вы можете оставить их отдельно друг от друга. В данном случае со временем мы планируем создать класс TestCase, однако вначале мы должны обособить части нашего примера.
Следующий этап – динамический вызов метода testMethod. Одной из приятных отличительных характеристик языка Python является возможность использования имен классов и методов в качестве функций (см. создание экземпляра класса WasRun). Получив атрибут, соответствующий имени теста, мы можем обратиться к нему, как к функции. В результате будет выполнено обращение к методу с соответствующим именем.
WasRun
class WasRun:
def __init__(self, name):
self.wasRun = None
self.name = name
def run(self):
method = getattr(self, self.name)
method()
Это еще один шаблон рефакторинга: разработать код, который работает с некоторым конкретным экземпляром, и обобщить его, чтобы он мог работать со всеми остальными экземплярами, для этого константы заменяются переменными. В данном случае роль константы играет не некоторое значение, а фиксированный код (имя конкретного метода). Однако принцип остается тем же. В рамках TDD эта проблема решается очень легко: методика TDD снабжает вас конкретными работающими примерами, исходя из которых можно выполнить обобщение. Это значительно проще, чем выполнять обобщение исходя только из собственных умозаключений.
Теперь наш маленький класс WasRun занят решением двух разных задач: во-первых, он следит за тем, был ли выполнен метод; во-вторых, он динамически вызывает метод. Пришло время разделить полномочия (разделить нашу работу на две разные части). Прежде всего, создадим пустой суперкласс TestCase и сделаем класс WasRun производным классом:
TestCase
class TestCase:
pass
WasRun
class WasRun(TestCase):.
Теперь переместим атрибут name из подкласса в суперкласс:
TestCase
def __init__(self, name):
self.name = name
WasRun
def __init__(self, name):
self.wasRun = None
TestCase.__init__(self, name)
Наконец, замечаем, что метод run() использует только атрибуты суперкласса, значит, скорее всего, он должен располагаться в суперклассе. (Я всегда стараюсь размещать операции рядом с данными.)
TestCase
def __init__(self, name):
self.name= name
def run(self):
method = getattr(self, self.name)
method()
Естественно, между выполнениями этих модификаций я каждый раз запускаю тесты, чтобы убедиться, что все работает как надо.
Нам надоело смотреть на то, как наша программа каждый раз печатает одно и то же: None и 1. Использовав разработанный механизм, мы можем теперь написать:
TestCaseTest
class TestCaseTest(TestCase):
def testRunning(self):
test = WasRun("testMethod")
assert(not test.wasRun)
test.run()
assert(test.wasRun)
TestCaseTest("testRunning"). run()
Вызов тестового метода
Вызов метода setUp перед обращением к методу
Вызов метода tearDown после обращения к методу
Метод tearDown должен вызываться даже в случае неудачи теста
Выполнение нескольких тестов
Отчет о результатах
Сердцем этого теста являются операторы print, превращенные в выражения assert, таким образом, вы можете видеть, что выполненная нами процедура – это усложненный шаблон рефакторинга «Выделение метода» (Extract Method).
Я открою вам маленький секрет. Шажки, которыми мы двигались от теста к тесту в данной главе, выглядят смехотворно маленькими. Однако до того, как получить представленный результат, я пытался выполнить разработку более крупными шагами и потратил на это около шести часов (конечно, мне пришлось тратить дополнительное время на изучение тонкостей языка Python). Я два раза начинал с нуля и каждый раз думал, что мой код сработает, однако этого не происходило. Я понял, что попытка пропустить самый начальный, самый примитивный этап разработки, – это грубейшее нарушение принципов TDD.
Конечно же, вы не обязаны постоянно перемещаться такими лилипутскими шажками. После того как вы освоите TDD, вы сможете двигаться вперед более уверенно, реализуя между тестами значительно больший объем функциональности. Однако чтобы в совершенстве освоить TDD, вы должны научиться перемещаться маленькими шажками тогда, когда это необходимо.
Далее мы планируем перейти к решению задачи обращения к методу setUp(). Однако вначале подведем итог.
В данной главе мы
• поняли, как начать работу со смехотворно малюсенького шага;
• реализовали функциональность путем создания фиксированного кода, а затем обобщения этого кода путем замены констант на переменные;
• использовали шаблон «Встраиваемый переключатель» (Pluggable Selector) и дали себе обещание не использовать его вновь в течение как минимум четырех месяцев, так как он существенно усложняет анализ кода;
• начали работу над инфраструктурой тестирования маленькими шажками.