что такое tdd в тестировании
Тестирование с использованием BDD
Введение
TDD и BDD
Я не буду тут ссылаться на статьи и презентации корифеев IT индустрии. Мне запомнилась одна фраза из Twitter по поводу TDD которая засела в моем сознании, и которая на мой взгляд четко и коротко характеризует TDD подход. К сожалению дословно я её привести не могу, но смысл в ней следующий: «если вы следуете TDD, то можете быть 100% уверены, что каждая строчка кода была написана благодаря упавшему(ым) тесту(ам)». Я видел и слышал много дебатов по поводу достоинств и недостатков TDD и BDD, но а) тесты писать надо б) если код был написан благодаря упавшему тесту, то этому коду можно доверять, и с легкостью его изменять (рефакторить) не боясь испортить поведение системы.
Теперь про BDD. Появилось это явление позже и как утверждает Фаулер в статье «Mocks Aren’t Stubs» благодаря так называемым мокистам. С другой стороны этот подход активно продвигают ребята из Agaile тусовки, сводя к минимуму расстояние между разработчиками, пользователями и аналитиками систем. Достигается это путем получения Executable Scenarios, иными словами, сценарии которые описывают пользователи переводятся в исполняемый тест. BDD frameworks с этим удачно справляются.
Теперь перейдем к сравнению.
Все примеры описывают один и тот же сценарий. Я опущу описание проблемы решение, которой необходимо покрыть тестами, потому как сами сценарии должны ясно описать её.
Автор статьи приводит реализации BDD в порядке возрастания симпатии к ним.
Easyb
Данный framework написан на Groovy. Как и все BDD реализации поддерживает нотацию Given-When-Then. Легко интегрируется в Continuous Integration (CI).
Вот пример сценария:
description «This story is about sqrt optimisation algorithm»
narrative «this shows sqrt optimisation», <
as a «java developer»
i want «to know how sqrt optimisation works»
so that «that I can pass google interview»
>
before «init input and expected result», <
>
scenario «find summ within two indexes #leftIndex and #rightIndex of the array #input», <
given «An Sqrt algorithm implementation», <
alg = new SqrtDecompositionSum(input.toArray(new int[0]))
>
when «calc sum between two indexes», <
actualSum = alg.calcSummBetween(leftIndex, rightIndex)
>
then «summ should be equal expected #expectedSumm», <
actualSum.shouldBe expectedSumm
>
>
Вот как выглядит результат теста:
Тут «вылазит» первый недостаток easyb. Дело в том, что непонятно откуда взялось 2 сценария, в то время как описан 1. Если вглядеться в секцию where сценария, то можно увидеть, что подготавливается 2 набора входных и ожидаемых значений. К сожалению конструкция where не документирована даже на сайте проекта, по крайней мере я её там не нашел.
Ниже приведен пример упавшего теста-сценария
Spock
Spock, на мой взгляд, ближе к разработчику нежели к аналитику или QA инженеру, однако все равно легко читаем.
Cucumber
С Cucumber я познакомился совсем недавно, и чем больше я с ним экспериментировал, тем больше он мне нравился. В отличие от первых двух, Cucumber выходец из Ruby. Существует его реализация для Java и С#.
Сценарии на cucumber состоят из двух файлов: собственно сценарий, и его реализация на Java, C#, Ruby. Это позволяет отделить сценарий от реализации, что делает сценарии абсолютно обычным повествованием на английском языке, приведем пример
Feature: Sqrt Sums Algorithm Feature
In order to ensure that my algorithm works
As a Developer
I want to run a quick Cuke4Duke test
Кстати, сценарии в cucumber называют features.
А вот реализация
public class SqrtsumsalgFeature <
private Algorithm alg;
private int result;
@Given («^The input array ([\\d\\s\\-\\,]*)$»)
public void theInputArray(String input) <
String[] split = input.split(«,»);
int[] arrayInput = new int[split.length];
for (int i = 0; i
Тут нужно соблюдать Naming Conventions как в именах файлов сценария и реализации, так и в именах методов реализации и шагов сценария. Иными словами они должны соответствовать. Соответствие достигается путем использования аннотаций Given, @When, Then и строк регулярных выражений в качестве аргументов к аннотациям.
Используя группы регулярных выражений можно выделять аргументы методов реализации.
Ниже приведен пример прошедшего теста на cucumber
А вот пример упавшего feature
Мне по-душе разделение сценария от его реализации. Кого то может смутить использование регулярных выражений чтобы «увязать» реализацию со сценарием, однако они скрыты от пишущего сценарий, и большинство разработчиков знакомы с ними, так что этот факт я бы не стал относить к недостаткам. За информацией о cuke4duke — реализации для Java прошу зайти сюда.
Что надо знать о TDD и BDD: как тесты помогают разработчикам работать в команде и понять заказчика
Java Developer в NIX
Вам знакома басня про лебедя, рака и щуку? Три совершенно разных существа тянули воз: лебедь пытался взлетать с ним, рак пятился с повозкой назад, а щука упорно тащила телегу на дно. Единства между ними не произошло, поэтому затея была обречена на провал — воз так и не сдвинулся с места.
Так аллегорично можно описать стандартный рабочий процесс, в котором заказчик хочет одно, руководитель проекта понимает задачу по-другому, а специалисты (дизайнеры, программисты, тестировщики) все делают по-своему… Иногда такие проекты доводят до конца. Однако зачастую они, как повозка из упомянутой басни, остаются на месте.
Чтобы в своей профессии вы не повторяли судьбу героев басни, были созданы методологии тестирования Test Driven Development (TDD) и Behavior Driven Development (BDD). Давайте разберемся, как они работают.
TDD и BDD: основные отличия модульного и интеграционного тестирования
Test Driven Development (TDD) — это разработка, основанная на тестировании. Предположим, вы получаете от заказчика запрос на добавление в разрабатываемый продукт нового функционала. Под этот запрос составляется техническая документация в виде тестов: в них согласованы и записаны новые требования, которые заказчик предъявляет к продукту.
Тестирование в TDD происходит через итерации (циклы) и соответствует такому порядку:
Все эти шаги TDD-разработки будут повторяться энное количество раз, пока специалисты не приведут код в соответствие с документацией и не удостоверятся в работоспособности новой функции.
Методология TDD относится к юнит-тестированию (модульному тестированию) и позволяет проверять отдельно взятые части продукта. Чаще всего TDD пишут сами разработчики, тесты реализуются в виде программного кода.
Но как протестировать не отдельный модуль продукта, а сложный сценарий с большим количеством условий и переменных?
В этом случае прибегают к использованию методологии Behavior Driven Development — разработке на основе поведения. В отличие от TDD, этот подход строится на написании нескольких пользовательских сценариев, под которые составляются тесты. BDD позволяет «предугадать», как поведет себя пользователь, используя продукт в соответствии с требованиями, которые записаны в технической документации. Порядок прохождения тестов схож с TDD. Единственное отличие — перед прохождением отдельных тестов формулируется ряд предварительных условий (сценариев), при которых они должны быть пройдены.
Сравненение BDD и TDD / TestLodge
Требования для BDD обычно составляет группа экспертов и не в виде программного кода, а словесно — на языке, понятном всем участникам проекта.
Behavior Driven Development относится к методам интеграционного тестирования. Оно позволяет понять, правильно ли взаимодействуют друг с другом отдельно взятые части программы.
Подход также эффективен в end-to-end-тестировании (Е2Е) и дает программистам представление о том, как функционирует вся разрабатываемая ими система. Получается, несмотря на то, что BDD и является расширением TDD-методологии, они имеют разное предназначение и реализуются разным набором инструментов.
В общем, мы друг друга поняли
Методологии BDD- и TDD- тестирования помогают достичь взаимопонимания между заказчиком продукта и всеми участниками, задействованными в его реализации.
Ч еткое следование прописанным заранее спецификациям позволяет избежать подводных камней в виде неоговоренных сценариев или разрозненной трактовки функционала продукта разными специалистами.
Весомое преимущество таких подходов в тестировании — отсутствие невалидных (недостоверных) сценариев. Каждый участник проекта еще на стадии проектирования может увидеть нереализуемые функции и рассказать об этом коллегам. Так можно исключить даже саму возможность создания неэффективного и бесполезного кода.
Еще один плюс — наличие технической документации. TDD и BDD предполагают четко структурированную документацию, которая помогает быстрее адаптироваться новичкам, зашедшим в проект, уже на этапе производства.
Разработка больше не зависит от человеческого фактора: чтобы понять, что происходило в проекте с момента его создания, достаточно открыть нужный файл.
Как составлять тесты?
Давайте подытожим кратким списком рекомендаций по составлению тестов:
Инструменты для реализации юнит-тестирования
Юнит-тестирование на Java осуществляется при помощи фреймворка JUnit. Он относится к семейству фреймворков xUnit и используется преимущественно для Test-Driven- и Behavior-Driven-разработки. На сегодня JUnit 5 — самая свежая версия фреймворка, которая совместима с версиями Java 8 и выше.
Это значит, что фреймворк поддерживает Stream API, лямбды, функциональные интерфейсы и массу других «плюшек», которые таит в себе Java 8+.
Характерным отличием JUnit 5 от своей предыдущей версии является возможность запускать сразу несколько раннеров для одного и того же класса (JUnit4 был способен выполнять только один класс-раннер). JUnit 5 состоит из трех отдельных пакетов, которые можно подключать независимо друг от друга:
JUnit 5 состоит из трех отдельных пакетов, которые можно подключать независимо друг от друга
Экстеншн-модели и аннотации в JUnit 5
Экстеншн-модель в JUnit Jupiter — это разновидность совершенно нового API, позволяющая расширять функционал отдельного теста и добавлять новые условия для работы с ним. Для работы с экстеншн-моделью существуют экстеншн-поинты.
Существует пять основных видов экстеншн-поинтов:
В JUnit пятой версии также расширен функционал Assertions API. Например, теперь вы сможете работать с методом assertAll, который построен на использовании лямбд. Он позволяет производить групповые проверки: каждая следующая проверка выполняется только в том случае, если предыдущая верна:
JUnit 5 также позволяет контролировать выполнение теста в зависимости от внешних условий. Вы можете выбирать ОС, на которой будете проводить тестирование, а также версию Java, на которой будет работать тест. Контролировать можно и то, при каких системных настройках будет запущен тест.
Также JUnit 5 помогает создавать свои собственные аннотации. Для этого необходимо указать @Target (ElementType.METHOD) для нового интерфейса и затем перечислить все аннотации, которые должны сработать при подключении вашей аннотации. Вот несколько примеров таких аннотаций:
Тестируй себя сам
Для любого разработчика очень важно уметь самостоятельно применять методы юнит-тестирования. Такой подход позволяет на ранних этапах уловить непонятные аспекты ТЗ до того, как будет реализован код. Помимо этого, TDD- и BDD- тестирование обеспечивает постоянную коммуникацию внутри команды: и исполнитель, и заказчик, и руководитель всегда будут находиться в одной плоскости понимания проекта. Именно в таком единстве всех участников процесса и заключается главное преимущество использования интеграционного и модульного тестирования.
Этот материал – не редакционный, это – личное мнение его автора. Редакция может не разделять это мнение.
TDD для начинающих. Ответы на популярные вопросы
На очередном собеседовании, спрашивая о TDD, я пришел к выводу, что даже основные идеи разработки через тесты не поняты большинством разработчиков. Я считаю, что незнание этой темы – большое упущение для любого программиста.
Мне задают много вопросов про TDD. Из этих вопрсов я выбрал ключевые и написал на них ответы. Сами вопросы вы можете найти в тексте, они выделены курсивом.
В качестве отправной точки мы будем решать бизнес-задачу:
Задача состоит из нескольких подзадач: 1) написать консольное приложение, которое отправляет отчеты. 2) Каждый второй сформированный отчет надо отправлять ещё и аудиторам. 3) Если ни одного отчета не сформировано, то отправляем сообщение руководству о том, что отчетов нет. 4) После отправки всех отчётов, нужно вывести в консоль количество отправленных.
Начинаем с того, что будем решать одну небольшую задачу. Мы будем описывать бизнес-требование в коде с помощью теста.
Итак, я создал новый проект консольного приложения (финальный код можно скачать из SVN). Сейчас в нём нет ни одной строчки кода. Надо придумать, как вообще будет работать моё приложение. Внимание, начало! Создаем тест, который описывает 4-ое требование:
public class ReporterTests
<
[Fact]
public void ReturnNumberOfSentReports()
<
var reporter = new Reporter();
var reportCount = reporter.SendReports();
Класс Assert проверяет равно ли количество отосланных отчетов 2. Тест запускается консольной утилитой xUnit, либо каким-нибудь плагином к Visual Studio.
Только что мы спроектировали API нашего приложения. Мы будем использовать объект Reporter с функцией SendReports. Функция SendReports возвращает количество отправленных отчетов, это показывает тест с помощью утверждения Assert.Equal. Если переменная reportCount не будет равена 2, то тест не пройдет.
На этом первый этап проектирования закончился, переходим к кодированию. Напишем минимум кода, чтобы этот тест сработал.
Вопрос: Сначала нам надо написать много тестов, а потом исправлять их один за другим?
Нет, идем последовательно. Не будем распылять свою усилия. В каждый момент времени только один неработающий тест. Напишем код, который этот тест починит и можем писать следующий тест.
В нашем проекте на данный момент есть только один класс ReporterTests. Пора создать тестируемый класс Reporter. Добавляем в проект объект Reporter и создаем у него пустую функцию SendReports. Для того, чтобы тест прошёл, функция SendReports должна вернуть цифру 2. Пока не понятно, как задать начальные условия в объекте Reporter, чтобы функция SendReports вернула цифру 2.
Возвращаемся к проектированию. Я думаю, что у меня будет отдельный класс для создания отчётов, и класс для отправки отчётов. Сам объект Reporter будет управлять логикой взаимодействия этих классов. Назовем первый объект IReportBuilder, а второй – IReportSender. Попроектировали, пора написать код:
[Fact]
public void ReturnNumberOfSentReports()
<
IReportBuilder reportBuilder;
IReportSender reportSender;
var reporter = new Reporter(reportBuilder, reportSender);
var reportCount = reporter.SendReports();
Вопрос: есть ли правила для именования тестовых методов?
Да, есть. Желательно, чтобы название тестового метода показывало, что проверяет тест и какого результата мы ожидаем. В данном случае название говорит нам: «Возвращается количество отправленных отчётов».
Как будут работать классы, реализующие эти интерфейсы, сейчас не имеет значения. Главное, что мы можем сформировать IReportBuilder‘ом все отчёты и отправить их с помощью IReportSender‘а.
Вопрос: почему стоит использовать интерфейсы IReportBuilder и IReportSender, а не создать конкретные классы?
Реализовать объект для создания отчётов и объект для отправки отчётов можно по-разному. Сейчас удобнее скрыть будущие реализации этих классов за интерфейсами.
Вопрос: Как задать поведение объектов, с которыми взаимодействует наш тестируемый класс?
Вместо реальных объектов, с которыми взаимодействует наш тестируемый класс удобнее всего использовать заглушки или mock-объекты. В текущем приложении мы будем создавать mock-объекты с помощью библиотеки Moq.
[Fact]
public void ReturnNumberOfSentReports()
<
var reportBuilder = new Mock ();
var reportSender = new Mock ();
// задаем поведение для интерфейса IReportBuilder
// Здесь говорится: «При вызове функции CreateReports вернуть List состоящий из 2х объектов»
reportBuilder.Setup(m => m.CreateRegularReports())
.Returns( new List < new Report(), new Report()>);
var reporter = new Reporter(reportBuilder.Object, reportSender.Object);
var reportCount = reporter.SendReports();
Запускаем тест – он не проходит, потому что мы не реализовали функцию SendReports. Программируем самую простую из возможных реализаций:
public class Reporter
<
private readonly IReportBuilder reportBuilder;
private readonly IReportSender reportSender;
public int SendReports()
<
return reportBuilder.CreateRegularReports().Count;
>
>
Запускаем тест и он проходит. Мы реализовали 4-ое требование. При этом записали его в виде теста. Таким образом, мы составляем документацию нашей системы. Как показала практика – эта документация самая актуальная в любой момент времени и никогда не устаревает. Идем дальше.
Вопрос: Есть ли стандартный шаблон для написания теста?
Да. Он называется Arrange-Act-Assert (AAA). Т.е. тест состоит из трех частей. Arrange (Устанавливаем) – производим настройку входных данных для теста. Act (Действуем) – выполняем действие, результаты которого тестируем. Assert (Проверяем) – проверяем результаты выполнения. Я подпишу соответствующие этапы в следующем тесте.
Теперь займёмся первым требованием – отправлением отчётов. Тест будет проверять, что все созданные отчёты отправлены:
[Fact]
public void SendAllReports()
<
// arrange
var reportBuilder = new Mock ();
var reportSender = new Mock ();
reportBuilder.Setup(m => m.CreateRegularReports())
.Returns( new List < new Report(), new Report()>);
var reporter = new Reporter(reportBuilder.Object, reportSender.Object);
// assert
reportSender.Verify(m => m.Send(It.IsAny ()), Times.Exactly(2));
>
Вопрос: Надо ли писать тесты для всех объектов приложения в одном тестовом классе?
Очень нежелательно. В этом случае тестовый класс разрастется до огромных размеров. Лучше всего на каждый тестируемый класс создать отдельный файл с тестами.
Запускаем тест, он не проходит, потому что мы не реализовали отправку отчётов в функции SendReports. На этом, как обычно мы проектировать заканчиваем и переходим к кодированию:
public int SendReports()
<
IList reports = reportBuilder.CreateRegularReports();
foreach (Report report in reports)
<
reportSender.Send(report);
>
Запускаем тесты – оба проходят. Мы реализовали ещё одно бизнес-требование. К тому же, запустив оба теста мы убедились, что не сломали функциональность, которую делали 5 минут назад.
Вопрос: Как часто надо запускать все тесты?
Чем чаще, тем лучше. Любое изменение в коде может неожиданно для вас отразится на других частях системы. Особенно, если этот код писали не Вы. В идеале все тесты должны запускаться автоматически системой интеграции (Continuous Integration) при каждой сборке проекта.
Вопрос: Как протестировать приватные методы?
Если вы дочитали до этого момента, то уже понимаете, что раз сначала пишутся тесты, а уже потом код, значит весь код внутри класса будет по-умолчанию протестирован.
Пора подумать о том, как реализовывать третье требование. С чего начнем? Нарисуем UML-диаграммы или просто помедитируем сидя в кресле? Начнём с теста! Запишем 3-е бизнес-требование в коде:
[Fact]
public void SendSpecialReportToAdministratorIfNoReportsCreated()
<
var reportBuilder = new Mock ();
var reportSender = new Mock ();
reportBuilder.Setup(m => m.CreateRegularReports()).Returns( new List ());
reportBuilder.Setup(m => m.CreateSpecialReport()).Returns( new SpecialReport());
var reporter = new Reporter(reportBuilder.Object, reportSender.Object);
reportSender.Verify(m => m.Send(It.IsAny ()), Times.Never());
reportSender.Verify(m => m.Send(It.IsAny ()), Times.Once());
>
Запускаем и убеждаемся, что тест не проходит. Теперь наши усилия направлены на починку этого теста. Здесь проектирование как обычно заканчивается и мы возвращаемся к программированию:
public int SendReports()
<
IList reports = reportBuilder.CreateRegularReports();
if (reports.Count == 0)
<
reportSender.Send(reportBuilder.CreateSpecialReport());
>
foreach (Report report in reports)
<
reportSender.Send(report);
>
Запускаем тесты – все 3 теста проходят. Мы реализовали новую функции и не сломали старые. Это не может не радовать!
Вопрос: Как узнать какой код уже протестирован?
Покрытие кода тестами можно проверить с помощью различных утилит. Для начала могу посоветовать PartCover.
Вопрос: Надо ли стремиться покрыть код тестами на 100%?
Нет. Это потебует слишком больших усилий на создание таких тестов и ещё больше на их поддержку. Нормальное прокрытие колеблется от 50 до 90%. Т.е. должна быть покрыта вся бизнес-логика без обращений к базе данных, внешним сервисам и файловой системе.
Второе требование я предлагаю реализовать вам самим и поделиться в комментариях финальной частью функции SendReports и вашего теста. Вы ведь сначала напишете тест, так?
Вопрос: Как же мне протестировать взаимодействие с базой данных, работу с SMTP-сервером или файловой системой?
Действительно, тестировать это нужно. Но это делается не модульными тестами. Потому что модульные тесты должны проходить быстро и не зависеть от внешних источников. Иначе вы будете запускать их раз в неделю. Более подробно об этом написано в статье «Эффективный модульный тест».
Вопрос: Когда я могу применять TDD?
TDD можно применять для создания любого приложения. Очень удобно его применять, если вы изучаете возможности новой библиотеки или языка программирования. Особых границ в применении нет. Возможны неудобства с тестированием многопоточных и других специфических приложений.
Заключение
Я желаю каждому разработчику попробовать эту практику. После этого можно решить, насколько TDD подходит для Вас лично и для проекта в целом.
Разработка через приемочные тесты (ATDD). Что это такое, и с чем его едят
Разработка через тестирование (TDD) – отличный способ повысить качество и надежность кода. Этот же подход может быть распространен и на разработку требований. Он называется «Разработка через приемочные тесты» – acceptance test driven development (ATDD). Сначала я присматривался к этому подходу, потом пробовал применить, потом долго тюнинговал, чтобы приспособить его под мои нужды, и теперь хочу поделиться мыслями. И для себя еще раз разложить все по полочкам.
В этой статье я расскажу небольшое введение в тему. Пример будет совсем простой и скорее для иллюстрации. А в следующей статье постараюсь поделиться историей, как я применял ATDD на практике при разработке настоящей фичи в реальном продукте.
Вместо введения
Когда я работал программистом в аутсорс компании на один банк, то мне приходилось изучать спецификации требований и оценивать трудоемкость задач. Оценивать нужно было как можно точнее, мы работали по модели оплаты за проект (Fixed Price), и все промахи в сроках были на нашей стороне и не оплачивались. Каждый раз, когда я читал спецификации, мне было все понятно, я не замечал в них нелогичные моменты, упущения, странности. Но как только начиналась разработка, то все косяки требований вылезали наружу, и было удивительно, как я их пропустил в начале. Несмотря на все усилия, я так и не мог придумать способ, как читать спецификации и находить в них проблемы до реализации.
Потом я перешел на работу в крупную компанию, которая занималась разработкой большого и сложного коробочного продукта, над которым работала огромная команда. Аналитики общались с партнерами и клиентами и записывали их пожелания. Потом эти спецификации, прежде чем быть взятыми в работу, проходили процедуру ревью, в которой участвовали и разработчики. Чтобы не тратить время на самой встрече, надо было сначала прочитать требования и подготовить вопросы. Как и в предыдущем проекте, большинство вопросов к содержимому документов возникали позднее – во время разработки, а не тогда, когда должны были возникнуть, то есть на этапе ревью.
Затем я ушел в свой стартап. Естественно, там не было никаких аналитиков, спецификаций и ревью. Обратную связь мы получали от пользователей в виде имейлов или звонков, тут же превращали это в фичи и включали в план разработки. Несмотря на отсутствие задокументированных требований, все равно приходилось оценивать трудоемкость задач. То, что на первый взгляд казалось очень простым, на деле становилось головной болью и наоборот. При быстром переключении контекста с одной проблемы на другую, уже реализованные решения вылетали из головы и становилось все труднее и труднее совмещать их в одном продукте. Нам было нужно какое-то подобие технической документации, тестпланов и требований. И чтобы стоило недорого.
Что такое ATDD
Acceptance test driven development (ATDD) является развитием идеи test driven development (TDD). Общий смысл в том, что прежде чем что-то делать, надо придумать критерий выполненной работы и критерий того, что работа сделана правильно. Почему это важно? Потому что эти критерии позволяют на самом раннем этапе понять, что именно требуется сделать, как это сделать, что именно считать хорошим результатом. Т.е. не выяснять детали по ходу дела, строя прототипы, а сразу приближаться к цели, так как цель уже определена, причем вполне формально.
Эти критерии описываются на понятном заказчику языке в виде готовых сценариев. Сценарии моделируют то, как проектируемая фича будет использоваться в дальнейшем. Если сценарий реализован и ожидаемый в нем результат может быт получен на практике, значит задача решена корректно и работу можно считать выполненной. Набор таких сценариев и называется приемочными тестами. Приемочные тесты фокусируются на поведении системы с точки зрения человека, а не на внутреннем устройстве и на технических деталях реализации.
Несмотря на общее название, этот подход относится ко вполне определенной части процесса – той, где происходит разработка требований и их формализация в спецификации. В данном процессе часто участвуют люди как со стороны бизнеса, так и с технической стороны, т.е. люди, обладающие разными компетенциями и взглядами на мир. Заказчики на интуитивном уровне понимают, что именно они хотят видеть в продукте, но сформулировать и перечислить требования кратко (и полно) могут далеко не все. Разработчики (представители исполнителя), в свою очередь, часто не знают, что именно забыл рассказать заказчик, и как это выяснить.
Для решения этих задач используется фреймворк Given – When – Then.
Фреймворк Given – When – Then
Смысл приемочного теста — показать, что произойдет с системой в конкретном сценарии. Для этого в сценарии должно быть описано, как выглядит система в начале теста, затем описывается какое-то действие, которое эту систему меняет. Это может быть воздействие снаружи, а может быть и какой-то внутренний триггер. В результате система немного меняет свое состояние, что и является критерием успешности. Важный момент: система рассматривается как черный ящик. Другими словами, формулируя тест, мы не знаем, как система устроена внутри и с чем она взаимодействует снаружи. Тут есть одна особенность. Иногда изменение системы недоступно для непосредственного наблюдения. Это означает, что саму приемку провести не получится. Выхода тут два — либо попытаться определить состояние косвенно, через какие-то соседние признаки, либо просто не использовать такой тест. Примером могут быть изменение полей в таблицах БД, отложенные изменения в каких-то недоступных файлах и т.д.
В юнит тестах используется шаблон Arrange – Act – Assert (AAA). Это означает, что в тестах должны быть явные части, отвечающие за подготовку данных — arrange, само действие, результат которого надо проверить – act, и собственно проверка, что реальность совпала с ожиданиями – assert. Для приемочных тестов используется подход Given – When – Then (GWT). Суть та же, только с другого ракурса.
Given часть может содержать в себе как одно, так и набор состояний. В случае, когда их несколько, эти состояния должны читаться через «И». Объединять какие-то состояния через «ИЛИ» можно, но тогда это будут два разных теста. Такое возможно для упрощения записи. Я рекомендую избегать этого до того момента, как все возможные комбинации состояний не будут описаны. Тогда можно быть уверенным, что ничего не забыто и слить несколько тестов в один для упрощения чтения и понимания. Это же справедливо и для Then — исходов, которые надо проверить может быть несколько. When должен быть один. Взаимовлияния триггеров лучше избегать.
GWT тесты вполне можно читать вслух: «Пусть (given) A и B, и C. Когда (when) случается D, то (then) получается E и F.». Их вполне можно использовать для документации или формулирования требований. Когда я говорю «читать», я не имею ввиду, что именно так они и должны быть записаны. В реальности такие тесты получаются очень масштабными. Если их записать простым текстом, то потом взглянуть на них системно очень тяжело. А без системы легко пропустить какие-нибудь важные сценарии.
Очень важный момент: формат записи нужно выбирать тот, который наиболее подходит к вашей задаче, с которым удобнее работать. Никаких ограничений тут нет. Given, when, then — это общая структура записи, то есть то, что обязательно должно быть в тесте, а непосредственное представление может быть любым – хоть предложения, хоть таблицы, хоть диаграммы.
ATDD не диктует правила, а предоставляет фреймворк для того, чтобы составить свою спецификацию через примеры. Есть модель черного ящика, GWT и их комбинирование. Все остальное является применением этих механизмов на практике, часть из которых можно считать устоявшимися.
Пример
Для примера возьмем что-нибудь простое и понятное, например, светофор. Как можно описать требования к разработке светофора с помощью GWT нотации? Для начала нужно понять, что именно в светофоре можно назвать Given, что является When, а что Then.
За состояние светофора можно принять информацию о том, какая секция сейчас горит. Светофор переключается (меняет состояние) через какие-то промежутки времени. Значит триггером является таймер, точнее, срабатывание таймера. Результатом срабатывания триггера является переход в одно из состояний. Т.е. можно считать, что в примере со светофором Given и Then – один и тот же набор:
Опишем поведение светофора в нотации GWT:
Вот 5 сценариев, прочитав которые, можно понять, как работает светофор. Естественно, у светофора есть еще куча режимов, например, режим желтого мигающего (когда он неисправен), или ручной режим управления (например, в случае ДТП) и т.д. Но не будем усложнять иллюстрацию.
Описывать тесты словами мне кажется избыточным. Тем более, что меняется в них только название цвета. Тут лучше подойдет диаграмма состояний или простая таблица:
Given | When | Then | |
---|---|---|---|
1 | Красный | Таймер | Красный + Желтый |
2 | Красный + Желтый | Таймер | Зеленый |
3 | Зеленый | Таймер | Зеленый мигающий |
4 | Зеленый мигающий | Таймер | Желтый |
5 | Желтый | Таймер | Красный |
Пример показывает один из основных преимуществ приемочных тестов: они позволяют общаться с бизнес пользователями практически на их языке. Приятным бонусом идет готовый набор сценариев для тестирования и последующей автоматизации.
Обеспечение полноты
Нотация Given — When — Then структурирует процесс составления тестов и дает уверенность в том, что тесты описывают все аспекты поведения системы. Не нужно сидеть и постоянно спрашивать себя: «А какой сценарий я еще не описал?».
Итак, алгоритм такой:
На каждом из этих этапов требуется участие заказчика или человека, который играет его роль, потому что именно он лучше всех представляет, что и как в итоге должно работать.
Почему полезно
Как уже было сказано, подобный подход, несмотря на свою избыточность, дает уверенность в том, что ни один из сценариев не будет пропущен. Это, пожалуй, главное преимущество такой формализации. Зачастую бизнес-пользователь видит процесс только в общих чертах и ему не видны детали. Я уверен, что вы постоянно слышите от заказчика или даже аналитика фразы типа: «Нам нужна такая-то фича, я все придумал, вот, смотри картинку», или «Тут нам нужна такая-то кнопка, у нас уже есть похожая функциональность в другом месте, сделай как там». Если до того, как начать разработку, сесть и прикинуть возможные варианты развития событий, то сразу всплывет очень много деталей, в которых, как известно, и кроется дьявол.
Подобный подход так же полезен и в случае, когда от аналитика приходит спека и ее нужно прочитать, дать свои оценки сложности и трудозатрат. При прочтении все детали ускользают, но если по ходу чтения вести конспект по форме GWT, то сразу становится понятно, какие сценарии плохо или неточно покрыты в требованиях и требуют уточнений.
Помимо анализа требований с целью разработки решения, GWT сценарии можно применять и для сбора требований. Предположим, что есть какая-то функциональная область и человек, который в ней разбирается, но время на общение с ним очень ограничено. Если подготовиться заранее и разобрать сценарии с помощью GWT фреймворка, то на самом интервью нужно будет узнать только то, что мы ничего не забыли из раздела Given, When и уточнить, что именно должно быть в разделе Then.
Есть специальные инструменты для автоматизации GWT сценариев, записанных в том числе и на естественных языках. Пример — cucumber. Я с ними не работал, поэтому ничего кроме факта их существования рассказать не могу.
Подводные камни
Обратная сторона мощности GWT — избыточность. Предположим, что вы определили N штук given, M штук when и K штук then. В худшем случае количество тестов будет огромным – N M K. И с этим надо как-то жить. Это верхняя оценка сложности; в реальности далеко не все эти сценарии будут осуществимы, а часть из них будет дублировать друг друга, а еще часть можно пропустить ввиду низкого приоритета или очевидности.
Вторым недостатком можно указать формат. По моему опыту могу сказать, что GWT записи всегда стремятся к минимализму. Во время их разработки не хочется тратить времени на детальные описания, потому что зачастую сценарии похожи друг на друга. В результате получается тяжело читаемая структура. После некоторого перерыва для ее понимания приходится восстанавливать контекст, и заново вспоминать условные сокращения и записи. Это также затрудняет задачу передать кому-то документ для ознакомления, так как, скорей всего, для его прочтения потребуется сам автор.
Следующий недостаток объясняет, почему ATDD скорее относится к области формализации требований с бесплатным бонусом в виде тестовых сценариев, а не собственно тестирования. Такие сценарии не могут описать композитные (большие и сложные) сценарии. Тестирование идеального черного ящика в первую очередь основано на аксиоме его идеальности. В реальности ящики черными не бывают, они всегда взаимодействуют с чем-то снаружи себя, являясь при этом частью более сложной системы — продукта. Легко можно переусложнить требования, если попытаться включить в один документ сразу все связи внутри продукта. Такой набор приемочных тестов будет настолько огромным и сложным, что мало чем сможет помочь. Поэтому, в реальной жизни сквозные сценарии в качестве приемочных тестов не применяются.
Немного исторической справки
Если верить Википедии, то идея формулировать спецификации через конкретные сценарии была впервые описана Ward Cunningham в 1996 году, а сам термин specification by example ввел Martin Fowler в 2004 году. Дальнейшее развитие идеи формулируется в книге «Bridging the Communication Gap: Specification by Example and Agile Acceptance Testing» от Gojko Adzic 2009 года. В 2011 он же выпустил еще одну книгу на эту тему: «Specification by Example: How Successful Teams Deliver the Right Software». Рекомендую эти книги для обращения к первоисточнику.