Книга: Чистый код: создание, анализ и рефакторинг. Библиотека программиста
Назад: 8. Границы
Дальше: 10. Классы

9. Модульные тесты

09_01.tif 

За последние десять лет наша профессия прошла долгий путь. В 1997 году никто не слыхал о методологии TDD (Test Driven Development, то есть «разработка через тестирование»). Для подавляющего большинства разработчиков модульные тесты представляли собой короткие фрагменты временного кода, при помощи которого мы убеждались в том, что наши программы «работают». Мы тщательно выписывали свои классы и методы, а потом подмешивали специализированный код для их тестирования. Как правило, при этом использовалась какая-нибудь несложная управляющая программа, которая позволяла вручную взаимодействовать с тестируемым кодом.

Помню, в середине 90-х я написал программу на C++ для встроенной системы реального времени. Программа представляла собой простой таймер со следующей сигнатурой:

void Timer::ScheduleCommand(Command* theCommand, int milliseconds)

Идея была проста; метод Execute класса Command выполнялся в новом программном потоке с заданной задержкой в миллисекундах. Оставалось понять, как его тестировать.

Я соорудил простую управляющую программу, которая прослушивала события клавиатуры. Каждый раз, когда на клавиатуре вводился символ, программа планировала выполнение команды, повторяющей этот же символ пять секунд спустя. Затем я настучал на клавиатуре ритмичную мелодию и подождал, пока эта мелодия «появится» на экране спустя пять секунд.

«Мне… нужна такая девушка… как та… которую нашел мой старый добрый папа…»

Я напевал эту мелодию, нажимая клавишу «.», а потом пропел ее снова, когда точки начали появляться на экране.

И это был весь тест! Я убедился в том, что программа работает, показал ее своим коллегам и выкинул тестовый код.

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

Когда у меня накопился бы пакет тестов, я бы позаботился о том, чтобы эти тесты были удобными для любого другого программиста, которому потребуется работать с моим кодом. Я бы проследил за тем, чтобы тесты и код поставлялись вместе, в одном исходном пакете.

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

Три закона TTD

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

Первый закон. Не пишите код продукта, пока не напишете отказной модульный тест.

Второй закон. Не пишите модульный тест в объеме большем, чем необходимо для отказа. Невозможность компиляции является отказом.

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

Эти три закона устанавливают рамки рабочего цикла, длительность которого составляет, вероятно, около 30 секунд. Тесты и код продукта пишутся вместе, а тесты на несколько секунд опережают код продукта.

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

О чистоте тестов

Несколько лет назад мне предложили заняться обучением группы, которая решила, что тестовый код не должен соответствовать тем же стандартам качества, что и код продукта. Участники группы сознательно разрешили друг другу нарушать правила в модульных тестах. «На скорую руку» — вот каким девизом они руководствовались. Разумно выбирать имена переменных не обязательно, короткие и содержательные тестовые функции не обязательны. Качественно проектировать тестовый код, организовать его продуманное логическое деление не обязательно. Тестовый код работает, охватывает код продукта — и этого вполне достаточно.

Пожалуй, некоторые читатели сочувственно отнесутся к этому решению. Возможно, кто-то в прошлом писал тесты наподобие тех, которые я написал для своего класса Timer. Примитивные «временные» тесты отделены огромным расстоянием от пакетов автоматизированного модульного тестирования. Многие программисты (как и та группа, в которой я преподавал) полагают, что тесты «на скорую руку» — лучше, чем полное отсутствие тестов.

Но на самом деле тесты «на скорую руку» равносильны полному отсутствию тестов, если не хуже. Дело в том, что тесты должны изменяться по мере развития кода продукта. Чем примитивнее тесты, тем труднее их изменять. Если тестовый код сильно запутан, то может оказаться, что написание нового кода продукта займет меньше времени, чем попытки втиснуть новые тесты в обновленный пакет. При изменении кода продукта старые тесты перестают проходить, а неразбериха в тестовом коде не позволяет быстро разобраться с возникшими проблемами. Таким образом, тесты начинают рассматриваться как постоянно растущий балласт.

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

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

И в определенном смысле они были правы. Их усилия по тестированию действительно оказались бесполезными. Но виной всему было их решение — небрежно написанные тесты привели к катастрофе. Если бы группа ответственно подошла к написанию тестов, то затраченное время не пропало бы даром. Я говорю об этом вполне уверенно, потому что работал (и преподавал) во многих группах, добившихся успеха с аккуратно написанными модульными тестами.

Мораль проста: тестовый код не менее важен, чем код продукта. Не считайте его «кодом второго сорта». К написанию тестового кода следует относиться вдумчиво, внимательно и ответственно. Тестовый код должен быть таким же чистым, как и код продукта.

Тесты как средство обеспечения изменений

Если не поддерживать чистоту своих тестов, то вы их лишитесь. А без тестов утрачивается все то, что обеспечивает гибкость кода продукта. Да, вы не ошиб­лись. Именно модульные тесты обеспечивают гибкость, удобство сопровождения и возможность повторного использования нашего кода. Это объясняется просто: если у вас есть тесты, вы не боитесь вносить изменения в код! Без тестов любое изменение становится потенциальной ошибкой. Какой бы гибкой ни была ваша архитектура, каким бы качественным ни было логическое деление вашей архитектуры, без тестов вы будете сопротивляться изменениям из опасений, что они приведут к появлению скрытых ошибок.

С тестами эти опасения практически полностью исчезают. Чем шире охват тестирования, тем меньше вам приходится опасаться. Вы можете практически свободно вносить изменения даже в имеющий далеко не идеальную архитектуру, запутанный и малопонятный код. Таким образом, вы можете спокойно улучшать архитектуру и строение кода!

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

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

Чистые тесты

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

В листинге 9.1 приведен фрагмент кода из проекта FitNesse. Эти три теста трудны для понимания; несомненно, их можно усовершенствовать. Прежде всего, повторные вызовы addPage и assertSubString содержат огромное количество повторяющегося кода [G5]. Что еще важнее, код просто забит второстепенными подробностями, снижающими выразительность теста.

Листинг 9.1. SerializedPageResponderTest.java

public void testGetPageHieratchyAsXml() throws Exception

{

  crawler.addPage(root, PathParser.parse("PageOne"));

  crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));

  crawler.addPage(root, PathParser.parse("PageTwo"));

 

  request.setResource("root");

  request.addInput("type", "pages");

  Responder responder = new SerializedPageResponder();

  SimpleResponse response =

      (SimpleResponse) responder.makeResponse(

         new FitNesseContext(root), request);

  String xml = response.getContent();

 

  assertEquals("text/xml", response.getContentType());

  assertSubString("<name>PageOne</name>", xml);

  assertSubString("<name>PageTwo</name>", xml);

  assertSubString("<name>ChildOne</name>", xml);

}

 

public void testGetPageHieratchyAsXmlDoesntContainSymbolicLinks()

  throws Exception

{

  WikiPage pageOne = crawler.addPage(root, PathParser.parse("PageOne"));

  crawler.addPage(root, PathParser.parse("PageOne.ChildOne"));

  crawler.addPage(root, PathParser.parse("PageTwo"));

 

  PageData data = pageOne.getData();

  WikiPageProperties properties = data.getProperties();

  WikiPageProperty symLinks = properties.set(SymbolicPage.PROPERTY_NAME);

  symLinks.set("SymPage", "PageTwo");

  pageOne.commit(data);

 

  request.setResource("root");

  request.addInput("type", "pages");

  Responder responder = new SerializedPageResponder();

  SimpleResponse response =

      (SimpleResponse) responder.makeResponse(

         new FitNesseContext(root), request);

  String xml = response.getContent();

 

  assertEquals("text/xml", response.getContentType());

  assertSubString("<name>PageOne</name>", xml);

  assertSubString("<name>PageTwo</name>", xml);

  assertSubString("<name>ChildOne</name>", xml);

  assertNotSubString("SymPage", xml);

}

 

public void testGetDataAsHtml() throws Exception

{

  crawler.addPage(root, PathParser.parse("TestPageOne"), "test page");

 

  request.setResource("TestPageOne");

  request.addInput("type", "data");

  Responder responder = new SerializedPageResponder();

  SimpleResponse response =

       (SimpleResponse) responder.makeResponse(

          new FitNesseContext(root), request);

  String xml = response.getContent();

 

  assertEquals("text/xml", response.getContentType());

  assertSubString("test page", xml);

  assertSubString("<Test", xml);

}

Например, присмотритесь к вызовам PathParser, преобразующим строки в экземпляры PagePath, используемые обходчиками (crawlers). Это преобразование абсолютно несущественно для целей тестирования и только затемняет намерения автора. Второстепенные подробности, окружающие создание ответчика, а также сбор и преобразование ответа тоже представляют собой обычный шум. Также обратите внимание на неуклюжий способ построения URL-адреса запроса из ресурса и аргумента. (Я участвовал в написании этого кода, поэтому считаю, что вправе критиковать его.)

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

Теперь рассмотрим усовершенствованные тесты в листинге 9.2. Они делают абсолютно то же самое, но код был переработан в более ясную и выразительную форму.

Листинг 9.2. SerializedPageResponderTest.java (переработанная версия)

public void testGetPageHierarchyAsXml() throws Exception {

  makePages("PageOne", "PageOne.ChildOne", "PageTwo");

 

  submitRequest("root", "type:pages");

 

  assertResponseIsXML();

  assertResponseContains(

    "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"

  );

}

 

public void testSymbolicLinksAreNotInXmlPageHierarchy() throws Exception {

  WikiPage page = makePage("PageOne");

  makePages("PageOne.ChildOne", "PageTwo");

 

  addLinkTo(page, "PageTwo", "SymPage");

 

  submitRequest("root", "type:pages");

 

  assertResponseIsXML();

  assertResponseContains(

    "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"

  );

  assertResponseDoesNotContain("SymPage");

}

 

public void testGetDataAsXml() throws Exception {

  makePageWithContent("TestPageOne", "test page");

 

  submitRequest("TestPageOne", "type:data");

 

  assertResponseIsXML();

  assertResponseContains("test page", "<Test");

}

В структуре тестов очевидно воплощен паттерн ПОСТРОЕНИЕ-ОПЕРАЦИИ-ПРОВЕРКА. Каждый тест четко делится на три части. Первая часть строит тестовые данные, вторая часть выполняет операции с тестовыми данными, а третья часть проверяет, что операция привела к ожидаемым результатам.

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

Любой программист, читающий эти тесты, очень быстро разберется в том, что они делают, не сбиваясь с пути и не увязнув в лишних подробностях.

Предметно-ориентированный  язык тестирования

Тесты в листинге 9.2 демонстрируют методику построения предметно-ориен­тированного языка для программирования тестов. Вместо вызова функций API, используемых программистами для манипуляций с системой, мы строим набор функций и служебных программ, использующих API; это упрощает написание и чтение тестов. Наши функции и служебные программы образуют специализированный API, то есть по сути — язык тестирования, который программисты используют для упрощения работы над тестами, а также чтобы помочь другим программистам, которые будут читать эти тесты позднее.

Тестовый API не проектируется заранее; он развивается на базе многократной переработки тестового кода, перегруженного ненужными подробностями. По аналогии с тем, как я переработал листинг 9.1 в листинг 9.2, дисциплинированные разработчики перерабатывают свой тестовый код в более лаконичные и выразительные формы.

Двойной стандарт

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

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

Листинг 9.3. EnvironmentControllerTest.java

@Test

  public void turnOnLoTempAlarmAtThreashold() throws Exception {

    hw.setTemp(WAY_TOO_COLD);

    controller.tic();

    assertTrue(hw.heaterState());

    assertTrue(hw.blowerState());

    assertFalse(hw.coolerState());

Листинг 9.3 (продолжение)

    assertFalse(hw.hiTempAlarm());

    assertTrue(hw.loTempAlarm());

  }

Конечно, этот листинг содержит множество ненужных подробностей. Например, что делает функция tic? Я бы предпочел, чтобы читатель не задумывался об этом в ходе чтения теста. Читатель должен думать о другом: соответствует ли конечное состояние системы его представлениям о «слишком низкой» температуре.

Обратите внимание: в ходе чтения теста вам постоянно приходится переключаться между названием проверяемого состояния и условием проверки. Вы смотрите на heaterState (состояние обогревателя), а затем ваш взгляд скользит налево к assertTrue. Вы смотрите на coolerState (состояние охладителя), а ваш взгляд отступает к assertFalse. Все эти перемещения утомительны и ненадежны. Они усложняют чтение теста.

В листинге 9.4 представлена новая форма теста, которая читается гораздо проще.

Листинг 9.4. EnvironmentControllerTest.java (переработанная версия)

@Test

  public void turnOnLoTempAlarmAtThreshold() throws Exception {

    wayTooCold();

    assertEquals("HBchL", hw.getState());

  }

Конечно, я скрыл функцию tic, создав более понятную функцию wayTooCold. Но особого внимания заслуживает странная строка в вызове assertEquals. Верхний регистр означает включенное состояние, нижний регистр — выключенное состояние, а буквы всегда следуют в определенном порядке: {обогреватель, подача воздуха, охладитель, сигнал о высокой температуре, сигнал о низкой температуре}.

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

Листинг 9.5. EnvironmentControllerTest.java (расширенный набор)

@Test

  public void turnOnCoolerAndBlowerIfTooHot() throws Exception {

    tooHot();

    assertEquals("hBChl", hw.getState());

  }

 

  @Test

  public void turnOnHeaterAndBlowerIfTooCold() throws Exception {

    tooCold();

    assertEquals("HBchl", hw.getState());

  }

 

  @Test

  public void turnOnHiTempAlarmAtThreshold() throws Exception {

    wayTooHot();

    assertEquals("hBCHl", hw.getState());

  }

 

  @Test

  public void turnOnLoTempAlarmAtThreshold() throws Exception {

    wayTooCold();

    assertEquals("HBchL", hw.getState());

  }

Функция getState приведена в листинге 9.6. Обратите внимание: эффективность этого кода оставляет желать лучшего. Чтобы сделать его более эффективным, вероятно, мне стоило использовать класс StringBuffer.

Листинг 9.6. MockControlHardware.java

public String getState() {

    String state = "";

    state += heater ? "H" : "h";

    state += blower ? "B" : "b";

    state += cooler ? "C" : "c";

    state += hiTempAlarm ? "H" : "h";

    state += loTempAlarm ? "L" : "l";

    return state;

  }

Класс StringBuffer некрасив и неудобен. Даже в коде продукта я стараюсь избегать его, если это не приводит к большим потерям; конечно, в коде из листинга 9.6 потери невелики. Однако следует учитывать, что приложение пишется для встроенной системы реального времени, в которой вычислительные ресурсы и память сильно ограничены. С другой стороны, в среде тестирования такие ограничения отсутствуют.

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

Одна проверка на тест

Существует точка зрения, согласно которой каждая тестовая функция в тесте JUnit должна содержать одну — и только одну — директиву assert. Такое ­правило может показаться излишне жестким, но его преимущества наглядно видны в листинге 9.5. Тесты приводят к одному выводу, который можно быстро и легко понять при чтении.

Но что вы скажете о листинге 9.2? В нем объединена проверка двух условий: что выходные данные представлены в формате XML и они содержат некоторые подстроки. На первый взгляд такое решение выглядит сомнительно. Впрочем, тест можно разбить на два отдельных теста, каждый из которых имеет собственную директиву assert, как показано в листинге 9.7.

Листинг 9.7. SerializedPageResponderTest.java (одна директива assert)

public void testGetPageHierarchyAsXml() throws Exception {

    givenPages("PageOne", "PageOne.ChildOne", "PageTwo");

 

    whenRequestIsIssued("root", "type:pages");

 

    thenResponseShouldBeXML();

  }

 

  public void testGetPageHierarchyHasRightTags() throws Exception {

    givenPages("PageOne", "PageOne.ChildOne", "PageTwo");

 

    whenRequestIsIssued("root", "type:pages");

 

    thenResponseShouldContain(

      "<name>PageOne</name>", "<name>PageTwo</name>", "<name>ChildOne</name>"

    );

  }

Обратите внимание: я переименовал функции в соответствии со стандартной схемой given-when-then [RSpec]. Это еще сильнее упрощает чтение тестов. К сожалению, такое разбиение приводит к появлению большого количества дублирующегося кода.

Чтобы избежать дублирования, можно воспользоваться паттерном ШАБЛОННЫЙ МЕТОД [GOF], включить части given/when в базовый класс, а части then — в различные производные классы. А можно создать отдельный тестовый класс, поместить части given и when в функцию @Before, а части then — в каждую функцию @Test. Но похоже, такой механизм слишком сложен для столь незначительной проблемы. В конечном итоге я предпочел решение с множественными директивами assert из листинга 9.2.

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

Одна концепция на тест

Пожалуй, более полезное правило гласит, что в каждой тестовой функции должна тестироваться одна концепция. Мы не хотим, чтобы длинные тестовые функции выполняли несколько разнородных проверок одну за другой. Листинг 9.8 содержит типичный пример такого рода. Этот тест следовало бы разбить на три независимых теста, потому что в нем выполняются три независимых проверки. Объединение их в одной функции заставляет читателя гадать, почему в функцию включается каждая секция, и какое условие проверяется в этой секции.

Листинг 9.8

    /**

     * Тесты для метода addMonths().

     */

    public void testAddMonths() {

        SerialDate d1 = SerialDate.createInstance(31, 5, 2004);

 

        SerialDate d2 = SerialDate.addMonths(1, d1);

        assertEquals(30, d2.getDayOfMonth());

        assertEquals(6, d2.getMonth());

        assertEquals(2004, d2.getYYYY());

 

        SerialDate d3 = SerialDate.addMonths(2, d1);

        assertEquals(31, d3.getDayOfMonth());

        assertEquals(7, d3.getMonth());

        assertEquals(2004, d3.getYYYY());

 

        SerialDate d4 = SerialDate.addMonths(1, SerialDate.addMonths(1, d1));

        assertEquals(30, d4.getDayOfMonth());

        assertEquals(7, d4.getMonth());

        assertEquals(2004, d4.getYYYY());

    }

Вероятно, три тестовые функции должны выглядеть так:

• Given: последний день месяца, состоящего из 31 дня (например, май).

1) When: при добавлении одного месяца, последним днем которого является 30-е число (например, июнь), датой должно быть 30-е число этого месяца, а не 31-е.

2) When: при добавлении двух месяцев, когда последним днем второго месяца является 31-е число, датой должно быть 31-е число.

• Given:  последний день месяца, состоящего из 30 дней (например, июнь).

1) When: при добавлении одного месяца, последним днем которого является 31-е число, датой должно быть 30-е число этого месяца, а не 31-е.

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

Таким образом, проблема не в множественных директивах assert в разных секциях листинга 9.8. Проблема в том, что тест проверяет более одной концепции. Так что, вероятно, лучше всего сформулировать это правило так: количество директив assert на концепцию должно быть минимальным, и в тестовой функции должна проверяться только одна концепция.

F.I.R.S.T.

Чистые тесты должны обладать еще пятью характеристиками, названия которых образуют приведенное сокращение.

Быстрота (Fast). Тесты должны выполняться быстро. Если тесты выполняются медленно, вам не захочется часто запускать их. Без частого запуска тестов проблемы не будут выявляться на достаточно ранней стадии, когда они особенно легко исправляются. В итоге вы уже не так спокойно относитесь к чистке своего кода, и со временем код начинает загнивать.

Независимость (Independent). Тесты не должны зависеть друг от друга. Один тест не должен создавать условия для выполнения следующего теста. Все тесты должны выполняться независимо и в любом порядке на ваше усмотрение. Если тесты зависят друг от друга, то при первом отказе возникает целый каскад сбоев, который усложняет диагностику и скрывает дефекты в зависимых тестах.

Повторяемость (Repeatable). Тесты должны давать повторяемые результаты в любой среде. Вы должны иметь возможность выполнить тесты в среде реальной эксплуатации, в среде тестирования или на вашем ноутбуке во время возвращения домой с работы. Если ваши тесты не будут давать однозначных результатов в любых условиях, вы всегда сможете найти отговорку для объяснения неудач. Также вы лишитесь возможности проводить тестирование, если нужная среда недоступна.

Очевидность (Self-Validating). Результатом выполнения теста должен быть логический признак. Тест либо прошел, либо не прошел. Чтобы узнать результат, пользователь не должен читать журнальный файл. Не заставляйте его вручную сравнивать два разных текстовых файла. Если результат теста не очевиден, то отказы приобретают субъективный характер, а выполнение тестов может потребовать долгой ручной обработки данных.

Своевременность (Timely). Тесты должны создаваться своевременно. Модульные тесты пишутся непосредственно перед кодом продукта, обеспечивающим их прохождение. Если вы пишете тесты после кода продукта, вы можете решить, что тестирование кода продукта создает слишком много трудностей, а все из-за того, что удобство тестирования не учитывалось при проектировании кода продукта.

Заключение

В этой главе мы едва затронули тему тестирования. Я думаю, что на тему чистых тестов можно было бы написать целую книгу. Для «здоровья» проекта тесты не менее важны, чем код продукта. А может быть, они еще важнее, потому что тесты сохраняют и улучшают гибкость, удобство сопровождения и возможности повторного использования кода продукта. Постоянно следите за чистотой своих тестов. Постарайтесь сделать их выразительными и лаконичными. Изобретайте тестовые API, которым отводится роль предметно-ориентированного языка тестирования, упрощающего написание тестов.

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

Литература

[RSpec]: RSpec: Behavior Driven Development for Ruby Programmers, Aslak Hellesy, David Chelimsky, Pragmatic Bookshelf, 2008.

[GOF]: Design Patterns: Elements of Reusable Object Oriented Software, Gamma et al., Addison-Wesley, 1996.

1 Professionalism and Test-Driven Development, Robert C. Martin, Object Mentor, IEEE Software, May/June 2007 (Vol. 24, No. 3), pp. 32–36;

.

См. «Избегайте мысленных преобразований», с. 47.

См. запись в блоге Дейва Астела (Dave Astel): .

Учебные материалы Object Mentor.

Назад: 8. Границы
Дальше: 10. Классы