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

В свое время я очень строго следил за выполнением этого правила. Мы вместе с Массимо Арнольди (Massimo Arnoldi) разрабатывали код, который взаимодействовал с набором курсов обмена валют, хранящимся в глобальной переменной. Для разных тестов требовалось использовать разные наборы данных, и в некоторых случаях курсы обмена валют должны были быть разными для разных тестов. Вначале мы пытались использовать для тестирования глобальную переменную, однако в конце концов нас это утомило, и однажды утром (смелые решения, как правило, приходят ко мне по утрам) мы решили передавать объект Exchange (в котором хранились курсы обмена) в качестве параметра везде, где это было необходимо. Мы думали, что нам придется модифицировать сотни методов. Однако дело кончилось тем, что мы добавили дополнительный параметр в десять или пятнадцать методов и по ходу дела подчистили другие аспекты дизайна.

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

Самошунтирование (Self Shunt)

Как можно убедиться в том, что один объект корректно взаимодействует с другим? Заставьте тестируемый объект взаимодействовать не с целевым объектом, а с вашим тестом.

Предположим, что вы хотите динамически обновлять зеленую полосу, отображаемую в рамках тестируемого пользовательского интерфейса. Если мы сможем подключить наш объект к объекту TestResult, значит, мы сможем получать оповещения о запуске теста, о том, что тест не сработал, а также о том, что весь набор тестов начал работу или, наоборот, завершил работу. Каждый раз, получив оповещение о запуске теста, мы можем выполнить обновление интерфейса. Вот соответствующий тест:


ResultListenerTest

def testNotification(self):

result = TestResult()

listener = ResultListener()

result.addListener(listener)

WasRun("testMethod"). run(result)

assert 1 == listener.count


Тест нуждается в объекте, который подсчитывал бы количество оповещений:


ResultListener

class ResultListener:

def __init__(self):

self.count = 0

def startTest(self):

self.count = self.count + 1


Подождите-ка! Зачем нам нужен отдельный объект Listener? Все необходимые функции мы можем возложить на объект TestCase. В этом случае объект TestCase становится подобием поддельного объекта.


ResultListenerTest

def testNotification(self):

self.count = 0

result = TestResult()

result.addListener(self)

WasRun("testMethod"). run(result)

assert 1 == self.count

def startTest(self):

self.count = self.count + 1


Тесты, написанные с использованием шаблона «Самошунтирование» (Self Shunt), как правило, читаются лучше, чем тесты, написанные без него. Предыдущий тест является неплохим примером. Счетчик был равен 0, а затем стал равен 1. Вы можете проследить за последовательностью действий прямо в коде теста. Почему счетчик стал равен 1? Очевидно, кто-то обратился к методу startTest(). Где произошло обращение к методу startTest()? Это произошло в начале выполнения теста. Вторая версия теста использует два разных значения переменной count в одном месте, в то время как первая версия присваивает переменной count значение 0 в одном классе и проверяет эту переменную на равенство значению 1 в другом.

Возможно, при использовании шаблона «Самошунтирование» (Self Shunt) вам потребуется применить шаблон рефакторинга «Выделение интерфейса» (Extract Interface), чтобы получить интерфейс, который должен быть реализован вашим тестом. Вы должны сами определить, что проще: выделение интерфейса или тестирование существующего объекта в рамках концепции «черный ящик». Однако я часто замечал, что интерфейсы, выделенные при выполнении самошунтирования, в дальнейшем, как правило, оказываются полезными для решения других задач.

В результате использования шаблона «Самошунтирование» (Self Shunt) вы можете наблюдать, как тесты в языке Java обрастают разнообразными причудливыми интерфейсами. В языках с оптимистической типизацией класс теста обязан реализовать только те операции интерфейса, которые действительно используются в процессе выполнения теста. Однако в Java вы обязаны реализовать абсолютно все операции интерфейса несмотря на то, что некоторые из них будут пустыми. По этой причине интерфейсы следует делать как можно менее емкими. Реализация каждой операции должна либо возвращать значение, либо генерировать исключение – это зависит от того, каким образом вы хотите быть оповещены о том, что произошло нечто неожиданное.

Строка-журнал (Log String)

Как можно убедиться в том, что обращение к методам осуществляется в правильном порядке? Создайте строку, используйте ее в качестве журнала. При каждом обращении к методу добавляйте в строку-журнал некоторое символьное сообщение.

Данный прием продемонстрирован ранее, в главе 20, где мы тестировали порядок обращения к методам класса TestCase. В нашем распоряжении имеется шаблонный метод, который, как мы предполагаем, обращается к методу setUp(), затем к тестовому методу, а затем к методу tearDown(). Нам хотелось бы убедиться в том, что обращение к методам осуществляется в указанном порядке. Реализуем методы так, чтобы каждый из них добавлял свое имя в строку-журнал. Исходя из этого можно написать следующий тест:


def testTemplateMethod(self):

test = WasRun("testMethod")

result = TestResult()

test.run(result)

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


Реализация тоже очень проста:


WasRun

def setUp(self):

self.log = "setUp "

def testMethod(self):

self.log = self.log + "testMethod "

def tearDown(self):

self.log = self.log + "tearDown "


Шаблон «Строка-журнал» (Log String) особенно полезен в случае, когда вы реализуете шаблон «Наблюдатель» (Observer) и желаете протестировать порядок поступления оповещений. Если вас прежде всего интересует, какие именно оповещения генерируются, однако порядок их поступления для вас не важен, вы можете создать множество строк, добавлять строки в множество при обращении к методам и в выражении assert использовать операцию сравнения множеств.

Шаблон «Строка-журнал» (Log String) хорошо сочетается с шаблоном «Самошунтирование» (Self Shunt). Объект-тест реализует методы шунтируемого интерфейса так, что каждый из них добавляет запись в строку-журнал, затем проверяется корректность этих записей.

Тестирование обработки ошибок (Crush Test Dummy)

Как можно протестировать работу кода, обращение к которому происходит в случае ошибки, возникновение которой маловероятно? Создайте специальный объект, который вместо реальной работы генерирует исключение.

Непротестированный код считается неработающим. Но что же тогда делать с кодом обработки ошибок? Надо ли его тестировать? Только в случае, если вы хотите, чтобы он работал.

Предположим, что мы хотим проверить, что происходит с нашим приложением в случае, если на диске не остается свободного места. Неужели для этого необходимо вручную создавать огромное количество файлов? Есть альтернатива. Мы можем сымитировать заполнение диска, фактически использовав шаблон «Подделка» (Fake It).

Вот наш тест для класса File:


private class FullFile extends File {

public FullFile(String path) {

super(path);

}

public boolean createNewFile() throws IOException {

throw new IOException();

}

}


Теперь мы можем написать тест ожидаемого исключения:


public void testFileSystemError() {

File f = new FullFile("foo");

try {

saveAs(f);

fail();

} catch (IOException e) {

}

}


Тест кода обработки ошибки напоминает шаблон «Поддельный объект» (Mock Object), однако в данном случае нам не надо подделывать весь объект. Для реализации этой методики удобно использовать анонимные внутренние классы языка Java. При этом вы можете переопределить только один необходимый вам метод. Сделать это можно прямо внутри теста, благодаря чему код теста станет более понятным:


public void testFileSystemError() {

File f = new File("foo") {

public boolean createNewFile() throws IOException {

throw new IOException();

}

};

try {

saveAs(f);

fail();

} catch (IOException e) {

}

}

Сломанный тест (Broken Test)

Как следует завершить сеанс программирования, если вы программируете в одиночку? Оставьте последний тест неработающим.

Этому приему научил меня Ричард Гэбриел (Richard Gabriel). Вы заканчиваете сеанс на середине предложения. Когда вы возвращаетесь к работе, вы смотрите на начало предложения и вспоминаете, о чем вы думали, когда писали его. Вспомнив ход своих мыслей, вы завершаете предложение и продолжаете работу. Если по возвращении к работе вам не надо завершать никакого предложения, вы вынуждены потратить несколько минут, чтобы сначала просмотреть уже написанный код, изучить список задач, вспомнить, над чем вы собирались работать, затем попытаться восстановить прежнее состояние ваших мыслей и наконец приступить к работе.

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