Экстремальное программирование. Разработка через тестирование — страница 18 из 41

Как можно протестировать выполнение метода tearDown()? Проще всего – использовать еще один флаг. Однако все эти флаги начинают сбивать меня с толку. Если мы будем использовать флаги, мы упустим один очень важный аспект: метод setUp() должен быть выполнен непосредственно перед обращением к тестовому методу, а метод tearDown() – непосредственно после обращения к тестовому методу. Чтобы убедиться в этом, я намерен изменить стратегию тестирования. Предлагаю создать миниатюрный журнал, в котором будет отмечаться последовательность выполнения методов. Каждый метод будет добавлять в конец журнала соответствующую запись. Таким образом, просмотрев журнал, мы сможем установить порядок выполнения методов.


Вызов тестового метода

Вызов метода setUp перед обращением к методу

Вызов метода tearDown после обращения к методу

Метод tearDown должен вызываться даже в случае неудачи теста

Выполнение нескольких тестов

Отчет о результатах

Строка журнала в классе WasRun


WasRun

def setUp(self):

self.wasRun = None

self.wasSetUp = 1

self.log = "setUp "


Теперь можно изменить метод testSetUp(), чтобы вместо флага он проверял содержимое журнала:


TestCaseTest

def testSetUp(self):

self.test.run()

assert("setUp " == self.test.log)


После этого мы можем удалить флаг wasSetUp. Мы также можем добавить в журнал запись о выполнении метода:


WasRun

def testMethod(self):

self.wasRun = 1

self.log = self.log + "testMethod "


В результате нарушается работа теста testSetUp(), так как в момент выполнения этого метода журнал содержит строку «setUp testMethod». Изменяем ожидаемое значение:


TestCaseTest

def testSetUp(self):

self.test.run()

assert("setUp testMethod " == self.test.log)


Теперь этот тест выполняет работу обоих тестов, поэтому можно удалить testRunning и переименовать testSetUp:


TestCaseTest

def setUp(self):

self.test = WasRun("testMethod")

def testTemplateMethod(self):

self.test.run()

assert("setUp testMethod " == self.test.log)


Мы используем экземпляр класса WasRun всего в одном месте, поэтому необходимо отменить добавленный ранее хитрый трюк, связанный с setUp():


TestCaseTest

def testTemplateMethod(self):

test = WasRun("testMethod")

test.run()

assert("setUp testMethod " == test.log)


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


Вызов тестового метода

Вызов метода setUp перед обращением к методу

Вызов метода tearDown после обращения к методу

Метод tearDown должен вызываться даже в случае неудачи теста

Выполнение нескольких тестов

Отчет о результатах

Строка журнала в классе WasRun


Теперь мы готовы к реализации метода tearDown(). Ага! Опять я вас поймал! Теперь мы готовы к тестированию метода tearDown():


TestCaseTest

def testTemplateMethod(self):

test = WasRun("testMethod")

test.run()

assert("setUp testMethod tearDown " == test.log)


Он потерпел неудачу. Чтобы заставить его работать, выполняем несложные добавления:


TestCase

def run(self, result):

result.testStarted()

self.setUp()

exec "self." + self.name + "()"

self.tearDown()


WasRun

def setUp(self):

self.log = "setUp "

def testMethod(self):

self.log = self.log + "testMethod "

def tearDown(self):

self.log = self.log + "tearDown "


Неожиданно мы получаем ошибку не в классе WasRun, а в классе TestCaseTest. У нас нет «пустой» реализации метода teardown() в классе TestCase:


TestCase

def tearDown(self):

pass


Мы начинаем получать пользу от разрабатываемой инфраструктуры. Замечательно! Никакого рефакторинга не требуется. Очевидная реализация, созданная нами после обнаружения ошибки, сработала, и код получился чистым.


Вызов тестового метода

Вызов метода setUp перед обращением к методу

Вызов метода tearDown после обращения к методу

Метод tearDown должен вызываться даже в случае неудачи теста

Выполнение нескольких тестов

Отчет о результатах

Строка журнала в классе WasRun


Далее мы перейдем к формированию отчета о результатах выполнения тестов. Вместо использования встроенного в Python механизма обработки ошибок мы планируем реализовать и использовать собственный механизм наблюдения за работой тестов.

В данной главе мы

• перешли от использования флагов к использованию журнала;

• создали тесты для метода tearDown() и реализовали этот метод с использованием нового механизма журналирования;

• обнаружили проблему и вместо того чтобы возвращаться назад, смело исправили ошибку.

21. Учет и контроль

Вызов тестового метода

Вызов метода setUp перед обращением к методу

Вызов метода tearDown после обращения к методу

Метод tearDown должен вызываться даже в случае неудачи теста

Выполнение нескольких тестов

Отчет о результатах

Строка журнала в классе WasRun


Метод tearDown() должен выполняться, даже когда в процессе выполнения теста возникло исключение. Однако чтобы добиться этого, мы должны перехватывать исключения. (Честно говоря, я уже пытался реализовать это, но у меня не получилось, и я вернул все на свои места.) Если при реализации перехвата исключений мы допустим ошибку, мы можем не заметить этого, так как стандартный механизм доклада об исключениях не будет функционировать.

Работая в стиле TDD, важно понимать, что особое значение имеет порядок, в котором вы реализуете тесты. Выбирая тест, над которым я буду работать дальше, я стараюсь выбрать тот, который, во-первых, послужит для меня источником новых знаний, а во-вторых, достаточно прост, чтобы я был уверен в том, что могу заставить его работать. Если я добиваюсь успешного выполнения этого теста, но захожу в тупик при реализации следующего, я вполне могу выполнить откат назад на два шага. Было бы неплохо, если бы среда разработки оказывала мне в этом помощь. Например, было бы неплохо, если бы в момент срабатывания всех тестов автоматически создавалась резервная копия всего исходного кода, с которым я работаю.

После выполнения всех тестов желательно получить информацию о том, как они выполнились, например: «запущено 5, неудачных 2: TestCaseTest.testFooBar – ZeroDivideException, MoneyTest.testNegation – AssertionError». Если тесты перестают выполняться или результаты перестают отображаться на экране мы, по крайней мере, сможем обнаружить ошибку. Однако наша инфраструктура не обязана знать обо всех разработанных тестах.

Пусть метод TestCase.run() возвращает объект класса TestResult с результатами выполнения теста (вначале тест будет только один, однако позже мы усовершенствуем этот объект).


TestCaseTest

def testResult(self):

test = WasRun("testMethod")

result = test.run()

assert("1 run, 0 failed" == result.summary())


Начнем с поддельной реализации:


TestResult

class TestResult:

def summary(self):

return "1 run, 0 failed"


Теперь сделаем так, чтобы в результате выполнения метода TestCase.run() возвращался объект класса TestResult:


TestCase

def run(self):

self.setUp()

method = getattr(self, self.name)

method()

self.tearDown()

return TestResult()


Теперь, когда все тесты выполнились успешно, можно сделать реализацию метода summary() реальной. Как и раньше, будем двигаться маленькими шажками. Для начала заменим количество выполненных тестов константой:


TestResult

def __init__(self):

self.runCount = 1

def summary(self):

return "%d run, 0 failed" % self.runCount


(Оператор % в языке Python является аналогом функции sprintf в языке C.) Однако runCount не может быть константой, это должна быть переменная, значение которой вычисляется исходя из количества выполненных тестов. Мы можем инициализировать эту переменную значением 0, а затем увеличивать ее на единицу при выполнении очередного теста.

TestResult

def __init__(self):

self.runCount = 0

def testStarted(self):

self.runCount = self.runCount + 1

def summary(self):

return "%d run, 0 failed" % self.runCount


Теперь мы должны позаботиться о вызове этого нового метода:


TestCase

def run(self):

result = TestResult()

result.testStarted()

self.setUp()

method = getattr(self, self.name)

method()

self.tearDown()

return result


Мы точно так же могли бы преобразовать константу «0», обозначающую количество тестов, потерпевших неудачу, в переменную, как сделали это с переменной runCount, однако существующие тесты этого не требуют. Поэтому напишем новый тест:


TestCaseTest