Книга: Экстремальное программирование. Разработка через тестирование
Фикстура[20] (Fixture)
Фикстура[20] (Fixture)
Как создаются общие объекты, которые используются в нескольких тестах? Конвертируйте локальные переменные из тестов в переменные-члены класса TestCase. Переопределите метод setUp() и инициализируйте в нем эти переменные (то есть выполните создание всех необходимых объектов).
Если мы привыкли удалять дублирование из функционального (тестируемого) кода, должны ли мы удалять его из тестирующего кода? Может быть.
Существует проблема: зачастую вам приходится писать больше кода для того, чтобы установить объекты, используемые тестируемым методом, в интересующее вас состояние. Код, инициализирующий объекты, часто оказывается одинаковым для нескольких тестов. Такие объекты называются фикстурой теста (используется также английский термин scaffolding – строительные леса, подмостки). Дублирование подобного кода – это плохо. Вот две основные причины:
• написание подобного кода требует дополнительного времени, даже если мы просто копируем блоки текста через буфер обмена. Но наша задача – добиться того, чтобы написание тестов занимало как можно меньше времени;
• если приходится вручную менять интерфейс, перед нами встает необходимость изменять его в нескольких разных тестах (именно этого всегда следует ожидать от дублирования).
Однако дублирование кода инициализации объектов обладает также некоторыми преимуществами. Если код инициализации располагается непосредственно рядом с тестирующими выражениями assert(), весь код теста можно прочитать от начала и до конца. Если мы выделили код инициализации в отдельный метод, нам приходится помнить о том, что этот метод вызывается, нам приходится вспоминать, как именно выглядят объекты, и только вспомнив все это, мы можем написать остальную часть теста.
Среда xUnit поддерживает оба стиля написания тестов. Если вы думаете, что читателям будет сложно вспомнить объекты фикстуры, вы можете разместить код создания фикстуры непосредственно в теле теста. Однако вы также можете переместить этот код в метод с названием setUp(). В этом методе вы можете создать все объекты, которые будут использоваться в тестовых методах.
Далее приводится пример, который слишком прост, чтобы мотивировать выделение общего кода фикстуры, но зато достаточно короток, чтобы поместиться в данной книге. Мы можем написать:
EmptyRectangleTest
public void testEmpty() {
Rectangle empty = new Rectangle(0,0,0,0);
assertTrue(empty.isEmpty());
}
public void testWidth() {
Rectangle empty = new Rectangle(0,0,0,0);
assertEquals(0.0, empty.getWidth(), 0.0);
}
(Помимо прочего здесь также демонстрируется версия assertEquals() для чисел с плавающей точкой, которая принимает третий параметр – точность сравнения.) Мы можем избавиться от дублирования, написав:
EmptyRectangleTest
private Rectangle empty;
public void setUp() {
empty = new Rectangle(0,0,0,0);
}
public void testEmpty() {
assertTrue(empty.isEmpty());
}
public void testWidth() {
assertEquals(0.0, empty.getWidth(), 0.0);
}
Общий код выделен в виде отдельного метода. Среда xUnit гарантирует, что метод setUp() объекта TestCase будет обязательно вызван перед обращением к любому тестовому методу этого объекта. Теперь тестовые методы выглядят проще, однако, прежде чем понять их смысл, мы должны вспомнить о существовании метода setUp() и уточнить, что происходит внутри этого метода.
Какой из этих двух стилей предпочтительней? Попробуйте использовать каждый из них. Я фактически всегда выделяю общий код фикстуры и перемещаю его в метод setUp(), однако у меня хорошая память. Те, кто читает мои тесты, часто жалуются, что им приходится вспоминать слишком о многом. Значит, возможно, мне следует выделять меньшей объем кода, чтобы сделать тесты более понятными.
Взаимоотношения между подклассами класса TestCase и экземплярами этих подклассов являются наиболее запутанной стороной инфраструктуры xUnit. Каждый новый тип фикстуры требует создания нового подкласса класса TestCase. Каждая новая фикстура создается внутри экземпляра подкласса, используется один раз, а затем уничтожается.
В предыдущем примере, если мы хотим написать тесты для непустого прямоугольника (Rectangle), нам придется создать новый класс, который можно назвать, например, NormalRectangleTest. У этого класса будет свой собственный метод setUp(), в котором будет создан новый экземпляр Rectangle, необходимый ему для тестирования. Этот экземпляр Rectangle будет соответствовать непустому прямоугольнику. В общем случае, если я хочу использовать несколько отличающуюся фикстуру, я создаю новый подкласс класса TestCase.
Это означает, что не существует прямого простого соответствия между классами тестов и функциональными (тестируемыми) классами. Иногда одна фикстура используется для тестирования нескольких классов (подобное случается нечасто). Иногда для тестирования единственного функционального класса требуется создать две или три фикстуры. На практике в большинстве случаев получается, что количество классов тестов приблизительно совпадает с количеством функциональных классов. Однако это происходит вовсе не потому, что для каждого функционального класса вы создаете один-единственный класс теста.