Разработка через тестирование на простом примере

Разработка через тестирование начинается с юнит-тестов, а не с кода. Из части Agile она доросла до самостоятельной дисциплины.


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

Тогда зачем тратить на них время?

Для метода TDD ответ очевиден: сегодняшние вложения в тесты дадут вознаграждение завтра при добавлении новой функциональности и рефакторинге. Метод предлагает писать юнит-тесты перед кодом. Идея появилась в середине 1990-х, а в 2003 опубликовали книгу Экстремальное программирование. Она объясняет понятие непрерывного рефакторинга для улучшения кода продукта.

Принципы разработки через тестирование

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

  • Красная. Код не компилируется? Пишем юнит-тест.
  • Зелёная. Реализация пишется в сжатые сроки. Появилось чистое и простое решение? Выполняйте его. В другом случае продукт будет улучшаться пошагово. Главная цель – получить зелёный цвет для юнит-тестов.
  • Рефакторинг. Не пренебрегайте данным этапом – он устраняет повторения и вводит возможность изменять архитектуру. Фаза не затрагивает поведение программы.

Три ступени реализуются пятью этапами.

Цикл занимает до 10 минут и повторяется до покрытия функциональности юнит-тестами. Кажется, что всё просто. Однако шаги должны выполняться с предельной строгостью для использования преимуществ методологии. Соблюдайте правила, и получите структурированный код. Продукт будет соответствовать необходимым принципам (KISS -–Keep it simple, stupid) без реализации ненужных функций (DRY – Don’t Repeat Yourself) благодаря непрерывному рефакторингу.

Чистые тесты

TDD – это не чудо, ведущее к оптимальному набору юнит-тестов без усилий. Помните, что в этой практике код продукта и тесты одинаково важны!

Чистый тест соблюдает 5 правил:

  • Скорость: он работает быстро для частых запусков.
  • Независимость: не зависят друг от друга.
  • Повторность:  воспроизводится в любой среде.
  • Самопроверка:  возвращает результат (Неудача или Успех) для быстрого и лёгкого заключения.
  • Своевременность: пишется в подходящий момент.

Поменяйте мышление

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

Методология обнаруживает баги на ранних стадиях, что снижает затраты на поиск решения. 80% – это минимум покрытия кода серией юнит-тестов. Следовательно, разработчик уверенно приступает к рефакторингу и постоянному улучшению.

Выбирайте правильные инструменты

Eclipse с нативной поддержкой JUnit – явное преимущество. Плагины MoreUnit и Infinitest рекомендуется использовать в управлении юнит-тестами. Последние выполняют тесты при каждом изменении кода автоматически, что упрощает циклы обратной связи – часть непрерывного юнит-тестирования. В повторяющемся цикле методологии, использование шаблонов кода для юнит-тестов экономит время.

Разработка через тестирование в действии

Решим специфичную задачу. Возьмём проблему преобразования арабских чисел в римские.

Сначала напишем класс RomanNumeralTest. Он содержит серии юнит-тестов программы. Первое требование – значение «1» выдаёт римскую «I»:

public class RomanNumeralTest {
   private static RomanNumeral romanNumeral;

   @BeforeClass
   public static void setUpBeforeClass() {
      romanNumeral = new RomanNumeral();
   }

   @Test
   public void testIntToRoman_1_is_I() {
      assertThat(romanNumeral.intToRoman(1), is("I"));
   }
}

При запуске получим ошибку компиляции:

Продолжайте писать код продукта для прохождения теста. Для этого ставим курсор в месторасположение класса RomanNumeral и нажимаем Ctrl+1  – сочетание горячих клавиш Eclipse. Оно предложит быстрое исправление для создания пустого класса RomanNumeral. Аналогичным способом пишем intToRoman – простейший метод, достаточный для возвращения значения «I»:

public class RomanNumeral {
   public String intToRoman(int arabic) {
      return "I";
   }
}

Тест выполнится. Двигайтесь по циклу.

Сейчас мы входим в фазу рефакторинга. Она пройдёт быстро потому, что в коде нет повторений и нечего улучшать. Цикл начинается с добавления нового теста:

@Test
public void testIntToRoman_2_is_II() {
   assertThat(romanNumeral.intToRoman(2), is("II"));
}

Для успешного прохождения измените метод intToRoman класса RomanNumeral:

public String intToRoman(int arabic) {
   if (arabic == 2) return "II";
   return "I"; 
}

Когда получаем зелёный цвет, двигаемся к рефакторингу. Предпочтительно иметь один выход для метода и использовать фигурные скобки для условия if:

public String intToRoman(int arabic) {
   String roman = "I";

   if (arabic == 2){
      roman = "II";
   }

   return roman; 
}

Тесты по-прежнему имеет зелёный цвет успеха. Мы расширяем их дополнительным требованием – числом 3, которое даёт римскую «III». Это делает неудачными текущие юнит-тесты. Для проверки пишем следующее:

public String intToRoman(int arabic) {
   String roman = "I";

   if (arabic == 2){
      roman = "II";
   } else if(arabic == 3) {
      roman = "III";
   }

   return roman; 
}

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

public String intToRoman(int arabic) {
   StringBuilder roman = new StringBuilder();

   while (arabic-- > 0) {
      roman.append("I");
   }

   return roman.toString();
}

Мы обнаруживаем, что алгоритм не поддерживает римскую X. Для этого добавим обработку арабской десятки:

@Test
public void testInToRoman_10_is_X() {
   assertThat(romanNumeral.intToRoman(10), is("X"));
}

Тест выполняется, а рефакторинг не нужен. Значение 10 и его римское представление XX потребуют ещё один тест. Он «сломает» все предыдущие. Пишем следующий код:

public String intToRoman(int arabic) {
   StringBuilder roman = new StringBuilder();

   if (arabic == 10) {
      roman.append("X");
   } else {
      while (arabic-- > 0) {
         roman.append("I");
      }
   }

   return roman.toString();
}

Он пройдёт серию тестов. Фаза рефакторинга позволяет нам вернуться на предыдущий шаг для оптимизации алгоритма преобразования римской X. Заметно, что использование цикла будет эффективнее условий if / else if. Код принимает следующий вид:

public String intToRoman(int arabic) {
   StringBuilder roman = new StringBuilder();

   if (arabic == 10) {
      roman.append("X");
   } else if (arabic == 20) {
      roman.append("XX");
   } else {
      while (arabic-- > 0) {
         roman.append("I");
      }
   }

   return roman.toString();
}

Код успешно пройдёт серию тестов. Фаза рефакторинга позволяет нам вернуться на предыдущий шаг для оптимизации алгоритма преобразования римской X. Заметно, что использование цикла будет эффективнее условий if / else if. В итоге код принимает следующий вид:

public String intToRoman(int arabic) {
   StringBuilder roman = new StringBuilder();

   while (arabic >= 10) {
      roman.append("X");
      arabic -= 10;
   }

   while (arabic-- > 0) {
      roman.append("I");
   }

   return roman.toString();
}

Junit горит зелёным, а рефакторинг не изменил внутренне поведение метода. Рассмотрев код продукта, мы заметим, что задачи для «I» и «X» выполняются одним способом. У нас появилась идея нового дизайна алгоритма: две таблицы, связанные индексами и содержащие римские и арабские цифры соответственно.

public static final int[] ARABIC_DIGITS = {10, 1};
public static final String[] ROMAN_DIGITS = {"X", "I"};

public String intToRoman(int arabic) {
   StringBuilder roman = new StringBuilder();

   for (int i = 0; i < ARABIC_DIGITS.length; i++) {
      while (arabic >= ARABIC_DIGITS[i]) {
         roman.append(ROMAN_DIGITS[i]);
         arabic -= ARABIC_DIGITS[i];
      }
   }

   return roman.toString();
}

С помощью 30 юнит-тест не сломать, поэтому необязательно изменять код продукта. Для чисел 11 и 33, которые образуются римскими цифрами X и I алгоритм также остаётся функциональным. К неудаче в соответствующем юнит-тесте приведёт «V». Пора начинать цикл разработки через тестирование заново! Первое решение – добавить «V» и её арабский эквивалент в таблицы, и проверить алгоритм. Тесты проходят? Тогда это правильное решение. Во время рефакторинга возникает вопрос: не лучше ли заменить две индексированные таблицы на Java Map? Значения упорядочиваются путём двух взаимосвязанных циклов, текущее решение предпочтительнее потому, что оно проще соответствует принципам KISS.

Разработка через тестирование продолжается и мы завершаем серию юнит-тестов.

После 10 шага получаем следующие таблицы:

ARABIC_DIGITS = {1000, 900, 500, 400, 100, 90, 50, 40, 10, 9, 5, 4, 1};
ROMAN_DIGITS = {“M”,”CM”,”D”,”CD”,”C”,”XC”,”L”,”XL”,”X”,”IX”,”V”,”IV”,”I”};

Добавление новых тестов с такими арабскими цифрами, как 1954 и 3949 не потребует никаких изменений метода intToRoman в коде продукта. Серия полученных юнит-тестов покрывает код максимально.

Заключение

Знакомство с разработкой через тестирование показало силу этой практики. Смена парадигмы начинается с обучения и завершается ростом производительности разработчика.

Ещё одно упражнение: добавьте метод обратного преобразования в рассмотренную проблему с римскими и арабскими числами. Используйте разработку через тестирование. Практика – средство прогресса!

Что вы думаете по поводу такого тестирования?

ЛУЧШИЕ СТАТЬИ ПО ТЕМЕ