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

Разработка через тестирование начинается с юнит-тестов, а не с кода. Из части 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 в коде продукта. Серия полученных юнит-тестов покрывает код максимально.

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

Заключение

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

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

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

МЕРОПРИЯТИЯ

Комментарии

ВАКАНСИИ

Добавить вакансию
QA-инженер
от 150000 RUB
Java Developer (Life Science project)
от 200000 RUB до 300000 RUB
Product manager
Москва, по итогам собеседования
Junior QA engineer
Самара, от 35000 RUB до 55000 RUB

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