TDD или Разработка через тестирование. Гайд по Test-Driven Development

TDD, Test-Driven Development — это разработка, управляемая тестами, или разработка через тестирование — это одна из техник разработки ПО (или практик дизайна кода).

TDD достаточно давно и достаточно успешно используется во многих командах разработки.

Очень кратко о TDD — здесь. А эта большая статья подробнее описывает историю возникновения TDD, цели этой практики, связь с тестированием, и преимущества этой практики.

История TDD

Исторически TDD — это одна из практик так называемого экстремального программирования (eXtreme Programming). Изобретение TDD (или скорее четкое теоретическое «оформление») обычно приписывают Кенту Беку, одному из первых «экстремальных программистов» и соавтору фреймворка JUnit.

Сама по себе идея как бы «разработки через тестирование» не была для программистов того времени необычной. Тестирование в цикле разработки уже оформилось в отдельный этап, но никто до этого не предлагал писать тесты до написания собственно кода, который нужно тестировать. Это кажется как бы контр-интуитивным (если рассматривать TDD как некую практику тестирования). Но как неоднократно отмечал сам автор (а вместе с ним и многие другие выдающиеся программисты), TDD зародился не как практика тестирования, а как практика проектирования ПО.

Целью первых программистов- «экстремалов» было «взять идею, которая хорошо работает, и довести ее до логического завершения». 

Парное программирование, то есть практика совместного написания кода двумя людьми на одном компьютере, также возникло из этого же движения программистов.

Как говорит сам автор метода (здесь видео его лекции по экстремальному программированию): «Когда я тестирую написанный мной код, то код становится лучше; а что если довести этот процесс до крайности — писать тесты до того как я напишу код?»

Вот так была создана TDD-практика, своеобразная маленькая революция в разработке, в рамках большой революции eXtreme Programming.

Это тестирование или программирование?

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

Подход «тесты, затем код» помогает программисту поставить себя на место пользователя, что упрощает создание качественных API. Также TDD помогает удобнее контролировать область применения, писать более короткий — но лучше сфокусированный код, и создавать легко сочетаемые модули.

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

«Самая простая и рабочая практика» — эту фразу вы услышите от опытных программистов, применяющих экстремальное программирование.

Еще одно преимущество TDD — быстрая обратная связь. Чтобы быстро понять, правильно ли работает код в целом, не обязательно проводить «большое» тестирование, потому что базовые тесты уже есть.

В eXtreme Programming большое внимание уделяется петлям (циклам) обратной связи. И среди них предпочтительны короткие циклы, дающие быстрое подтверждение «все ОК». Из всех практик XP — TDD имеет второй по скорости цикл обратной связи (уступая только парному программированию), поскольку обеспечивает обратную связь в течение нескольких минут.

Еще одна интересная особенность TDD — это его не очень заметное ограничение, которое заставляет разработчиков «двигаться мелкими шагами». Те, кто давно знаком с TDD, наверняка знают Три закона TDD Роберта К. Мартина, также известного как Дядя Боб.

В одной из своих знаменитых статей дядя Боб объясняет TDD, формулируя 3 простых закона:

  • Вы должны написать падающий тест до написания production-кода.
  • Вы не должны писать больше тестов, чем достаточно (для того, чтобы они упали или не скомпилировались).
  • Вы не должны писать больше production-кода, чем достаточно (для того, чтобы прошел текущий падающий тест).

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

В данном контексте он представляет собой наиболее важную фазу всего цикла, когда все внимание — на качество кода. В eXtreme Programming существует концепция Simple Design, то есть постоянное стремление создавать код достаточно простым для дальнейшего совершенствования.

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

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

TDD — это практика проектирования кода

TDD не является чем-то новым. Существует множество команд и разработчиков, которые работают по TDD, обеспечивая скорость разработки. Есть несколько исследований, подтверждающих ее эффективность.

Интересный вопрос, который часто всплывает, связан с внедрением этой практики. Кривая обучения может быть крутой, но ее нельзя игнорировать. Так же как и затраты времени в написание тестов; несколько исследований показывают позитивный ROI.

Факты свидетельствуют, что после того как разработчики получили достаточную и необходимую теоретическую подготовку по TDD, проекты в большинстве случаев дают хорошие результаты. Это говорит о том, что TDD способствует успеху проектов, продуктов и команд.

Распространенное предубеждение против TDD связано с принципом «сначала тесты». Говорят, что есть сценарии, в которых подход «сначала тесты» не имеет смысла, а в других случаях это очень сложно (если не невозможно). С первым утверждением не поспоришь, TDD не является универсальным инструментом. Но у него есть свое предназначение: улучшать дизайн программного решения на фундаментальном уровне.

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

TDD на примере

Как начать работать по TDD? Попробуем объяснить на примере.

Предположим, что нам нужно написать программное обеспечение для кассового аппарата. Первая требуемая функциональность связана со сканером товаров: при прохождении через кассу (сканировании) определенного товара, например яблока, система должна взимать определенную сумму, например 50 центов.

Первой пользовательской историей может быть такая:

As a cashier, 
I want a basic checkout system
so I can let my customers pay for apples

Это и есть критерии приемки:

* When I scan an apple, the system charges 50 cents
* When I scan 3 apples, the system charges 150 cents

Используя TDD-подход, первое, что здесь нужно сделать — написать тест, который покажет, что на данный момент у нас нет системы проверки, которая может отсканировать товар:

import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.assertEquals;

public class CashRegisterTest {

  @Test
  public void When_I_add_an_apple_the_system_charges_50_cents() {
    //ARRANGE
    CashRegister register = new CashRegister();
    long expectedCost = 50;

    //ACT
    register.add("apple");
    long actualCost = register.getAmount();

    //ASSERT
    assertEquals(expectedCost, actualCost);
  }
}

Пример на Java. Он позволяет нам обратить внимание на следующие моменты.

Тестовые библиотеки

В первых строках мы видим ссылки на библиотеки JUnit. JUnit — фреймворк тестирования Java-проектов, один из многих фреймворков xUnit.

Для тех, кто начинает изучать TDD, одним из первых этапов изучения являются тестовые фреймворки. Существует много вариантов, и для языка который вы знаете, есть как минимум один «xUnit-подобный» фреймворк.

Тестовые классы и методы

Очень важно включать тесты в предназначенные для этого файлы. В Java обычной практикой является создание тестовых классов в специальных пакетах test:

Классы и методы тестирования составляют тестовый набор, или тест-сьют, который представляет собой набор тестов, сопровождающих программное обеспечение. Поэтому очень важно уделять должное внимание упорядочиванию тестового набора. Хороший тестовый набор группирует тесты по областям их применения; что позволяет группировать их по типу и предназначению, например отдельно юнит-тесты и сквозные.

Такое разделение позволяет запускать тесты, когда это нужно, и независимо друг от друга. Это также упрощает их запуск с помощью триггеров или расписаний в системах непрерывной интеграции.

Этап Красного Теста

Итак, мы написали тест:

 @Test
  public void When_I_add_an_apple_the_system_charges_50_cents() {
    //ARRANGE
    CashRegister register = new CashRegister();
    long expectedCost = 50;

    //ACT
    register.add("apple");
    long actualCost = register.getAmount();


    //ASSERT
    assertEquals(expectedCost, actualCost);
  }

Обратите внимание на аннотацию @Test: она позволяет тест-раннеру фреймворка JUnit понять, какие части кода должны быть выполнены и проверены.

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

Наконец, у нас есть «Три A», которые определяют качество теста, то есть ААА-паттерн (Настрой-Действуй-Проверь). Первая A — Arrange — напоминает нам о том, что сначала нужно настроить тест, создав объекты и все переменные, которые понадобятся для выполнения теста.

Вторая A, Act, сосредоточена на строках кода, которые «активируют», вводят в действие нашу тестируемую систему (System Under Test, SUT); в данном случае это объект CashRegister.

Наконец, третья буква A, то есть Assert, представляет собой главную точку: здесь мы определяем ожидаемый результат теста. Если утверждение окажется истинным, наш тест будет выполнен.

В этом примере мы написали первый тест функциональности, но не упомянули о production-коде. Здесь нет ошибки: первый тест (и последующие) в TDD-методике пишутся в предположении наличия production-кода, даже если он еще не написан.

Такой подход, поначалу довольно контринтуитивный, является одной из характерных особенностей TDD. В TDD вполне нормально писать код, который даже не компилируется, чтобы получить так называемый «красный тест», то есть тест, который изначально упадет, но подскажет, каким должен быть финальный код.

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

Этап Зеленого Теста

Что надо сделать, чтобы тест прошел? Очевидно, имплементировать объект CashRegister. Ниже приведена простая имплементация:

public class CashRegister {

    public void add(String good) {
        //actually I don't need this at the moment 🙂
    }

    public long getAmount() {
        return 50;
    }

}

Имплементация может показаться слишком простой, но этот код позволяет системе пройти тест. Понятно, что имплементация CashRegister нуждается в доработке, потому что в таком виде она работает, только если клиент покупает одно яблоко.

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

Этап рефакторинга

После того как утверждение (assertion) в тесте выполнено, наступает время рефакторинга.

Для краткости давайте забежим вперед и пропустим несколько циклов TDD, которые позволили бы нам прийти к следующей ситуации:

  • Система может обрабатывать четыре продукта: яблоко, грушу, ананас и банан.
  • Для некоторых продуктов существует специальное предложение: 3 яблока стоят 130 центов вместо 150, 2 груши — 45 центов вместо 60.

Посмотрим на имплементацию кода:

Вы видите дополнительные тесты, которые позволяют нам понемногу дописывать код. Они все зеленые, но код начинает «скрипеть»: количество условных операторов растет, и код становится «жестким».

Можно провести рефакторинг кода, введя, например, понятие PriceRule, которое определяет цену на каждый товар с учетом всех текущих скидок.

Посмотрим на результат рефакторинга:

Благодаря зеленым тестам, выполняющим роль «страховочной сетки», можно рефакторить код, внедрив новую абстракцию (PriceRule). Кроме того, использование Java Streams сделало операции фильтрации товаров более выразительными. Теперь добавление новых товаров в каталог значительно упростилось: по сути, достаточно реализовать новое правило PriceRule.

Этой статьи недостаточно, чтобы дать глубокое объяснение всех аспектов TDD, но этот небольшой пример дает представление о пользе TDD.

Дополнительные преимущества TDD

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

Цель TDD — дать программисту быструю обратную связь по коду, который он только что написал; если что-то не так, тест об этом сообщит. Другая цель TDD — сделать возможным рефакторинг, как мы только что видели: вы не откладываете рефакторинг на ближайшее будущее (а может, и на отдаленное…), увеличивая технический долг; вы действуете в моменте, когда разум еще свеж, а рефакторинг дешевле и проще.

Тесты, получаемые в TDD, обычно относятся к семейству юнит-тестов. Такие тесты работают быстро: порядка миллисекунд. Результатом такого подхода являются сотни, а то и тысячи тестов. Они постоянно выполняются на рабочей станции программиста и в CI/CD-пайплайне.

Стили TDD

Со временем развивались различные подходы к разработке, а вместе с ними и различные стили TDD.

Существует два основных подхода к TDD. Первый — это подход классической школы (то есть из классического Экстремального Программирования, и имеет три названия: классический, чикагский стиль, или Inside-out). Второй родился в Лондоне несколькими годами позже: мокистский, лондонский, или Outside-in.

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

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

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

Например, посмотрим на этот тест, написанный с использованием синтаксиса Gherkin в полном стиле Behavior Driven Development (BDD):

Feature: Supermarket Bundles        
           To increase revenues as a Supermarket Manager,
           I want to apply discounts and special offers to my customers

@Bundles
Scenario: if a customer buys 3 apples, they pay $1.00
    Given a apple costs 50 cents 
    Given the "buy 3 apples, pay 2" promotion
    When the cashier scans 3 apples
    Then the cash register charges $1.00

Возвращаясь к нашему предыдущему упражнению, мы знаем, что не существует имплементации вариантов «купи три товара, заплати за два». Подход Outside-in заставляет разработчиков думать о полной функциональности, реализуя ее так просто, как они могут, симулируя некоторые части заглушками, когда это необходимо. Внутри большого красного теста, как этот, разработка продолжается по классическим коротким TDD-циклам. Разработка продолжается до тех пор, пока этот тест не станет зеленым, что подтвердит правильность имплементации нужной функциональности.

Учитывая большое количество такого рода тестов, разработчики часто используют тестовые дублеры во время написания и имплементации. Наиболее часто применяются моки (отсюда название «мокист»).

Выводы

В этой статье изложены основные концепции, лежащие в основе TDD:

  • Фокус на проектировании кода
  • Маленькие шаги
  • Короткие петли обратной связи
  • Непрерывный рефакторинг

Это отличает TDD от других техник тестирования.

Источник


Какой была ваша первая зарплата в QA и как вы искали первую работу?

Мега обсуждение в нашем телеграм-канале о поиске первой работы. Обмен опытом и мнения.

Подписаться
Уведомить о
guest

0 комментариев
Межтекстовые Отзывы
Посмотреть все комментарии

Мы в Telegram

Наш официальный канал
Полезные материалы и тесты
Готовимся к собеседованию
Project- и Product-менеджмент

? Популярное

? Telegram-обсуждения

Наши подписчики обсуждают, как искали первую работу в QA. Некоторые ищут ее прямо сейчас.
Наши подписчики рассказывают о том, как не бояться задавать тупые вопросы и чувствовать себя уверенно в новой команде.
Обсуждаем, куда лучше податься - в менеджмент или по технической ветке?
Говорим о конфликтных ситуациях в команде и о том, как их избежать
$1100*
медианная зарплата в QA в июне 2023

*по результатам опроса QA-инженеров в нашем телеграм-канале

Собеседование

19%*
IT-специалистов переехало или приняло решение о переезде из России по состоянию на конец марта 2022

*по результатам опроса в нашем телеграм-канале

live

Обсуждают сейчас