Введение в BDD

Разработка на основе поведений с применением SpecFlow и WatiN

Брэндон Сэтром

Загрузить образец кода

По мере распространения автоматизированного модульного тестирования (unit testing) в разработке программного обеспечения то же самое происходит и с различными способами «сначала тест» (test-first methods). Каждая из этих методик открывает уникальный набор возможностей группам разработки и ставит перед ними свои проблемы, но все они нацелены на «тестирование как часть проектирования».

Однако в эпоху «сначала тестирование» поведение пользователя выражалось в основном через модульные тесты, написанные на языке системы, т. е. на языке, непонятном пользователю. С появлением методик разработки на основе поведений (Behavior-Driven Development, BDD) эта динамика меняется. Используя методики BDD, вы можете создавать автоматизированные тесты на языке бизнеса, в то же время сохраняя связь с реализуемой системой.

Конечно, был создан целый ряд инструментов, помогающих реализовать BDD в процессе разработки. К ним относятся Cucumber в Ruby, а также SpecFlow и WatiN для Microsoft .NET Framework. SpecFlow упрощает написание и выполнение спецификаций в Visual Studio, а WatiN позволяет управлять браузером для комплексного автоматизированного тестирования системы.

В этой статье я дам краткий обзор BDD, а затем поясню, как цикл BDD обертывает цикл традиционной разработки, управляемой тестами (Test-Driven Development, TDD), тестами уровня конкретной функциональности (далее — функциями), которые управляют реализацией уровня модулей. Подготовив почву для методов «сначала тестирование», я ознакомлю вас с SpecFlow и WatiN и покажу примеры того, как можно использовать эти инструменты совместно с MSTest для реализации BDD в ваших проектах.

Краткая история автоматизированного тестирования

Одна из наиболее ценных методик, родившихся в рамках инициативы Agile Software (гибкое ПО), — автоматизированная разработка в стиле «сначала тест», которую часто называют разработкой, управляемой тестами (Test-Driven Development, TDD). Основополагающий принцип TDD заключается в том, что тест — это не только руководство по проектированию и разработке; он в равной мере обеспечивает верификацию и регрессию. Тест также определяет модуль с необходимой функциональностью и потом используется для написания лишь того кода, который необходим для получения этой функциональности. Поэтому первый шаг в реализации любой новой функциональности — описание ваших ожиданий с помощью заведомо неуспешного теста (failing test) (рис. 1).

image: The Test-Driven Development Cycle

Рис.1. Цикл разработки, управляемой тестами (TDD)

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

Стив Фримен (Steve Freeman) и Нэт Прайс (Nat Pryce) в своей книге «Growing Object-Oriented Software, Guided by Tests» (Addison-Wesley Professional, 2009) отмечают, что «традиционной» TDD не хватает некоторых преимуществ истинной разработки по принципу «сначала тест».Вот небольшая цитата из их книги.

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

В 2006 году Дэн Норт (Dan North) документировал многие из этих проблем в статье в журнале «Better Software» ((blog.dannorth.net/introducing-bdd). В этой статье он описал ряд методик, которые он применял за предшествующие три года. Эти методики, хоть и являлись TDD по определению, привели его к принятию тестирования с большей ориентацией на анализ; в итоге он придумал термин «Behavior-Driven Development», который заключал в себе суть этих перемен.

В одном из распространенных применений BDD предпринимается попытка расширить TDD сужением фокуса и ужесточением процесса создания тестов через Acceptance Tests (приемочные тесты), или исполняемые спецификации (executable specifications). Каждая спецификация действует как входная точка в цикл разработки и описывает, как должна вести себя система с точки зрения пользователя (в пошаговом виде). После этого разработчик использует спецификацию и существующий процесс TDD для реализации ровно такого количества производственного кода, которого достаточно для получения базового сценария (рис. 2).

image: The Behavior-Driven Development Cycle

Рис. 2 Цикл разработки на основе поведений (BDD)

Где начинается проектирование

Многие считают BDD надмножеством TDD, но не его заменой. Ключевая разница — фокусировка на начальном проекте и создании тестов. Но основное внимание уделяется не тестам модулей или объектов, как в TDD, а целям пользователей и пошаговым операциям, предпринимаемым ими для достижения этих целей. Поскольку я больше не начинаю с тестов малых модулей, я менее склонен размышлять над деталями проекта или использованию более мелких частей. Вместо этого я документирую исполняемые спецификации, которые контролируют мою систему. Я по-прежнему пишу модульные тесты, но в BDD поощряется подход «от внешнего к внутреннему» (outside-in approach), который начинается с полного описания подлежащей реализации функции.

Давайте рассмотрим пример этого различия. В традиционной практике TDD вы могли бы написать тест (рис. 3) для проверки метода Create объекта CustomersController.

Рис.3. Модульный тест для создания объекта клиента

[TestMethod]
public void PostCreateShouldSaveCustomerAndReturnDetailsView() {
  var customersController = new CustomersController();
  var customer = new Customer {
    Name = "Hugo Reyes",
    Email = "hreyes@dharmainitiative.com",
    Phone = "720-123-5477" 
  };

  var result = customersController.Create(customer) as ViewResult;

  Assert.IsNotNull(result);
  Assert.AreEqual("Details", result.ViewName);
  Assert.IsInstanceOfType(result.ViewData.Model, typeof(Customer));

  customer = result.ViewData.Model as Customer;
  Assert.IsNotNull(customer);
  Assert.IsTrue(customer.Id > 0);
}

В случае TDD это обычно один из первых тестов. Я проектирую открытый API для своего объекта CustomersController, указывая ожидания того, как он должен вести себя. В случае BDD я все равно пишу этот тест, но не первым. Вместо этого я переношу фокус внимания на функциональность уровня функции (feature), расписывая нечто вроде того, что показано на рис. 4. Затем я использую этот сценарий как руководство при реализации каждого модуля кода, необходимого для успешного прохождения этого сценария.

Рис.4. Спецификация уровня функции

Feature: Create a new customer
  In order to improve customer service and visibility
  As a site administrator
  I want to be able to create, view and manage customer records

Scenario: Create a basic customer record
  Given I am logged into the site as an administrator
  When I click the "Create New Customer" link
  And I enter the following information
    | Field | Value                       |
    | Name  | Hugo Reyes                  |
    | Email | hreyes@dharmainitiative.com |
    | Phone | 720-123-5477                |
  And I click the "Create" button
  Then I should see the following details on the screen:
    | Value                       |
    | Hugo Reyes                  |
    | hreyes@dharmainitiative.com |
    | 720-123-5477                |

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

BDD и автоматизированное тестирование

С самого начала сообщество BDD искало способ обеспечения того же уровня автоматизации тестирования с приемочными тестами, который уже какое-то время является нормой в модульном тестировании. Один из примечательных примеров — Cucumber (cukes.info), средство тестирования на основе Ruby, которое выражает создание приемочных тестов уровня функции (feature), написанных на «понятном бизнесу языке предметной области».

Тесты Cucumber пишутся с применением синтаксиса User Story (пользовательская история) для каждого файла функции (feature file) и с синтаксисом Given, When, Then (GWT) для каждого сценария. (Подробнее о синтаксисе User Story см. по ссылке c2.com/cgi/wiki?UserStory.) GWT описывает текущий контекст сценария (Given), операции, выполняемые как часть теста (When) и ожидаемые видимые результаты (Then). Функция на рис. 4 является примером такого синтаксиса.

В Cucumber файлы функции на языке пользователя анализируются, и каждый этап сценария соответствует коду на Ruby, который использует открытые интерфейсы рассматриваемой системы и определяет, проходит ли проверку этот этап.

Инновационные достижения последних лет, позволяющие использовать сценарии как автоматизированные тесты, распространились и на экосистему .NET Framework. У разработчиков теперь есть инструменты, позволяющие писать спецификации на том же структурированном английском синтаксисе, что и в Cucumber, которые можно потом использовать как тесты, проверяющие код. Такие BDD-средства тестирования, как SpecFlow (specflow.org), Cuke4Nuke ((github.com/richardlawrence/Cuke4Nuke) и другие, позволяют создавать в процессе сначала исполняемые спецификации, использовать их по мере написания функциональности и заканчивать с документированной функцией (feature), прямо увязанной с вашим процессом разработки и тестирования.

Начинаем работу с SpecFlow и WatiN

В этой статье я буду использовать SpecFlow для тестирования приложения на основе шаблона Model-View-Controller (MVC). Чтобы начать работу с SpecFlow, нужно его скачать и установить. После установки SpecFlow создайте новое приложение ASP.NET MVC с проектом модульных тестов. Я предпочитаю, чтобы в моем проекте модульных тестов содержались именно тесты модулей (тесты контроллера, хранилища и т. д.), поэтому я также создаю проект приемочных тестов AcceptanceTests для тестов SpecFlow.

После включения проекта AcceptanceTests и добавления ссылки на сборку TechTalk.SpecFlow добавьте новую Feature, используя Add | New Item Templates, создаваемые SpecFlow при установке, и присвойте ей имя CreateCustomer.feature.

Заметьте, что этот файл создается с расширением .feature и что Visual Studio распознает его как поддерживаемый благодаря интегрируемому инструментарию SpecFlow. Также обратите внимание на то, что с файлом вашей функции сопоставлен файл отделенного кода (code-behind file) с расширением .cs. Всякий раз, когда вы сохраняете файл .feature, SpecFlow разбирает его содержимое и преобразует текст из этого файла в своего рода «испытательный стенд» (test fixture). Этот «испытательный стенд» представлен кодом в связанном файле .cs, и именно этот код выполняется всякий раз, когда вы запускаете свой набор тестов.

По умолчанию SpecFlow использует NUnit в качестве исполняющей среды тестов (test-runner), но также поддерживает MSTest — нужно лишь внести небольшие изменения в конфигурацию, добавив к проекту тестов файл app.config со следующими элементами:

<configSections>
  <section name="specFlow"
    type="TechTalk.SpecFlow.Configuration.ConfigurationSectionHandler, TechTalk.SpecFlow"/>
</configSections>
<specFlow>
  <unitTestProvider name="MsTest" />
</specFlow>

Ваш первый приемочный тест

Когда вы создаете новую функцию (feature), SpecFlow заполняет ее файл текстом по умолчанию, иллюстрируя синтаксис, применяемый для описания функции. Замените текст по умолчанию в своем файле CreateCustomer.feature текстом с рис. 4.

Каждый файл функции состоит из двух частей. Первая часть — имя и описание функции, где применяется синтаксис User Story для описания роли пользователя, его цели и типов операций, которые нужно выполнять пользователю, чтобы добиться указанной цели в системе. Этот раздел требуется SpecFlow для автоматической генерации тестов, но само содержимое не используется в этих тестах.

Вторая часть — это один или более сценариев. Каждый сценарий используется для генерации метода теста в сопоставленном файле .feature.cs, как показано на рис. 5, и каждый этап в сценарии передается в исполняющую среду SpecFlow, которая сопоставляет этот этап через регулярное выражение с записью в файле SpecFlow, который называется файлом определения этапа (Step Definition file).

Рис.5. Метод теста, генерируемый SpecFlow

public virtual void CreateABasicCustomerRecord() {
  TechTalk.SpecFlow.ScenarioInfo scenarioInfo = 
    new TechTalk.SpecFlow.ScenarioInfo(
    "Create a basic customer record", ((string[])(null)));

  this.ScenarioSetup(scenarioInfo);
  testRunner.Given(
    "I am logged into the site as an administrator");
  testRunner.When("I click the \"Create New Customer\" link");

  TechTalk.SpecFlow.Table table1 = 
    new TechTalk.SpecFlow.Table(new string[] {
    "Field", "Value"});
  table1.AddRow(new string[] {
    "Name", "Hugo Reyesv"});
  table1.AddRow(new string[] {
    "Email", "hreyes@dharmainitiative.com"});
  table1.AddRow(new string[] {
    "Phone", "720-123-5477"});

  testRunner.And("I enter the following information", 
    ((string)(null)), table1);
  testRunner.And("I click the \"Create\" button");

  TechTalk.SpecFlow.Table table2 = 
   new TechTalk.SpecFlow.Table(new string[] {
  "Value"});
  table2.AddRow(new string[] {
    "Hugo Reyes"});
  table2.AddRow(new string[] {
    "hreyes@dharmainitiative.com"});
  table2.AddRow(new string[] {
    "720-123-5477"});
  testRunner.Then("I should see the following details on screen:", 
    ((string)(null)), table2);
  testRunner.CollectScenarioErrors();
}

Определив свою первую функцию, нажмите Ctrl+R, T для запуска тестов SpecFlow. Ваш тест CreateCustomer закончится неудачей как незавершенный, так как SpecFlow не сможет найти соответствующее определение для первого этапа в вашем тесте (рис. 6). Заметьте, что исключение происходит в файле .feature, а не в связанном с ним файле отделенного кода.

image: SpecFlow Cannot Find a Step Definition

Рис. 6. SpecFlow не может найти определение этапа

Поскольку вы еще не создали файл определения этапа, это исключение ожидаемое. Щелкните OK в диалоге исключения и найдите тест CreateABasicCustomerRecord в окне Visual Studio Test Results. Если соответствующий этап не найден, SpecFlow использует ваш файл функции для генерации кода, необходимого в файле определения вашего этапа; вы можете скопировать этот код и использовать его, приступая к реализации этапа.

В проекте AcceptanceTests создайте файл определения этапа, используя шаблон SpecFlow Step Definition, и присвойте ему имя CreateCustomer.cs. Затем скопируйте вывод из SpecFlow в этот класс. Вы заметите, что каждый метод дополняется одним из атрибутов SpecFlow, который помечает метод как этап Given, When или Then и предоставляет RegEx, сопоставляющее метод с этапом в файле функции.

Интеграция WatiN для тестирования через браузер

Отчасти цель использования BDD заключается в создании набора автоматизированных тестов, которые проверяют максимально возможный объем функциональности всей системы. Поскольку я создаю приложение ASP.NET MVC, я могу использовать средства, помогающие писать сценарии для веб-браузера, чтобы он взаимодействовал с моим сайтом.

Одно из таких средств — WatiN, библиотека с открытым исходным кодом для автоматизации тестирования через веб-браузер. Вы можете скачать WatiN с from watin.sourceforge.net и добавить ссылку на WatiN.Core в свой проект AcceptanceTests.

Основной способ взаимодействия с WatiN — через объект браузера (либо IE(), либо FireFox() в зависимости от того, какой именно браузер вы предпочитаете), который предоставляет открытый интерфейс для управления экземпляром установленного браузера. Поскольку вы должны проходить браузер через несколько этапов в сценарии, вам нужен некий способ передачи того же объекта браузера между этапами в классе определения этапа. С этой целью я обычно создаю статический класс WebBrowser как часть своего проекта AcceptanceTests и использую его для работы с IE-объектом в WatiN и ScenarioContext, который применяется SpecFlow для сохранения состояния между этапами в сценарии:

public static class WebBrowser {
  public static IE Current {
    get {
      if (!ScenarioContext.Current.ContainsKey("browser"))
        ScenarioContext.Current["browser"] = new IE();
      return ScenarioContext.Current["browser"] as IE;
    }
  }
}

Первый этап, реализуемый вами в CreateCustomer.cs, — Given, который начинает тест, регистрируя пользователя как администратора сайта:

[Given(@"I am logged into the site as an administrator")]
public void GivenIAmLoggedIntoTheSiteAsAnAdministrator() {
  WebBrowser.Current.GoTo(http://localhost:24613/Account/LogOn);

  WebBrowser.Current.TextField(Find.ByName("UserName")).TypeText("admin");
  WebBrowser.Current.TextField(Find.ByName("Password")).TypeText("pass123");
  WebBrowser.Current.Button(Find.ByValue("Log On")).Click();

  Assert.IsTrue(WebBrowser.Current.Link(Find.ByText("Log Off")).Exists);
}

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

На этом этапе я использую WatiN, чтобы открыть Internet Explorer, перейти на страницу Log On сайта, заполнить поля User Name и Password, а затем нажать кнопку Log On. Когда я вновь запускаю тесты, окно Internet Explorer открывается автоматически, и я могу наблюдать, как WatiN взаимодействует с сайтом, «щелкая» ссылки и вводя текст (рис. 7).

image: The Browser on Autopilot with WatiN

Рис. 7. Браузер на автопилоте в WatiN

Этап Given теперь проходит проверку, и я на шаг ближе к реализации функции. Но после этого SpecFlow потерпит неудачу на первом этапе When, так как этот этап пока не реализован. Вы можете реализовать его таким кодом:

[When("I click the \" (.*)\" link")]
public void WhenIClickALinkNamed(string linkName) {
  var link = WebBrowser.Link(Find.ByText(linkName));

  if (!link.Exists)
    Assert.Fail(string.Format(
      "Could not find {0} link on the page", linkName));

  link.Click();
}

Теперь, когда я вновь запускаю тесты, они терпят неудачу, потому что WatiN не может найти ссылку с текстом «Create New Customer» на странице. Просто добавьте ссылку с этим текстом на основную страницу, и следующий этап успешно пройдет тест.

Вы уже почувствовали систему? SpecFlow поощряет к использованию той де методологии Red-Green-Refactor, которая является основным элементом методов разработки по принципу «сначала тест». Гранулярность каждого этапа в функции выступает в роли виртуальных ограничителей для реализации, стимулируя вас реализовать только ту функциональность, которая необходима для успешного прохождения проверки данного этапа.

А как насчет TDD внутри процесса BDD? К этому моменту я работаю только на уровне страницы и еще должен реализовать функциональность, которая создает запись о клиенте. Ради краткости давайте прямо сейчас реализуем остальные этапы ( рис. 8).

Рис. 8. Остальные этапы в определении этапов

[When(@"I enter the following information")]
public void WhenIEnterTheFollowingInformation(Table table) {
  foreach (var tableRow in table.Rows) {
    var field = WebBrowser.TextField(
      Find.ByName(tableRow["Field"]));

    if (!field.Exists)
      Assert.Fail(string.Format(
        "Could not find {0} field on the page", field));
    field.TypeText(tableRow["Value"]);
  }
}

[When("I click the \"(.*)\" button")]
public void WhenIClickAButtonWithValue(string buttonValue) {
  var button = WebBrowser.Button(Find.ByValue(buttonValue));

  if (!button.Exists)
    Assert.Fail(string.Format(
      "Could not find {0} button on the page", buttonValue));

  button.Click();
}

[Then(@"I should see the following details on the screen:")]
public void ThenIShouldSeeTheFollowingDetailsOnTheScreen(
  Table table) {
  foreach (var tableRow in table.Rows) {
    var value = tableRow["Value"];

    Assert.IsTrue(WebBrowser.ContainsText(value),
      string.Format(
        "Could not find text {0} on the page", value));
  }
}

Я вновь запускаю тесты, и они теперь заканчиваются неудачей, потому что у меня нет страницы, где можно вводить информацию о клиентах. Чтобы обеспечить создание записей о клиентах, нужна страница Create Customer View. Для создания такого представления в ASP.NET MVC требуется CustomersController. Мне также нужен новый код, а значит, я выхожу из внешнего цикла BDD и попадаю во внутренний цикл TDD, как показано на рис. 2.

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

Написание тестов модулей для реализации этапов

Создав класс теста CustomerControllersTests в проекте UnitTest, вы должны создать метод теста, который проверяет функциональность, предоставляемую CustomersController. Точнее, вам нужно создать новый экземпляр Controller, вызвать его метод Create и убедиться, что вы получили от него корректные View и Model:

[TestMethod]
public void GetCreateShouldReturnCustomerView() {
  var customersController = new CustomersController();
  var result = customersController.Create() as ViewResult;

  Assert.AreEqual("Create", result.ViewName);
  Assert.IsInstanceOfType(
    result.ViewData.Model, typeof(Customer));
}

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

public ActionResult Create() {
  return View("Create", new Customer());
}

Если вы перезапустите тесты SpecFlow, вы продвинетесь чуть дальше, но ваша Feature все еще не проходит тест. На этот раз тест провалится потому, что у вас нет страницы представления Create.aspx. Если вы добавите ее вместе с нужными полями, как указано в спецификации функции, вы сделаете еще один шаг на своем пути.

Процесс «от внешнего к внутреннему» (outside-in process) для реализации этой функциональности Create выглядит примерно, как показано на рис. 9.

image: Scenario-to-Unit Test Process

Рис. 9. Процесс «от сценария к модульному тесту»

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

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

Следуя тому же процессу, что и раньше, создайте тест, используя код модульного теста с рис. 3. Для компиляции теста добавьте пустой метод Create.После запуска тест провалится.После этого заполните тело метода Create:

[AcceptVerbs(HttpVerbs.Post)]
public ActionResult Create(Customer customer) {
  _repository.Create(customer);

  return View("Details", customer);
}

Мой объект Controller является просто контроллером, а само создание записи о клиенте осуществляется объектом Repository, которому известно внутреннее устройство хранилища данных. Я выкинул эту реализацию из статьи для краткости, но здесь важно отметить, что в реальном сценарии необходимость того, чтобы объект Repository сохранял запись о клиенте, должна запускать вложенный цикл модульного тестирования. Если вам нужен доступ к любому объекту, совместно работающему с вашим объектом, и этот объект пока не существует или не имеет требуемой вам функциональности, то вы должны следовать тому же циклу модульного тестирования, что и в случае ваших контроллеров и функции.

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

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

Пара слов о рефакторинге

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

Ищите возможности рефакторинга кода и в проекте AcceptanceTests. Вы обнаружите, что некоторые этапы имеют тенденцию к частому повторению в различных функциях, особенно этапы Given. С помощью SpecFlow эти этапы можно легко поместить в раздельные файлы определения этапов, организованные по функции, например LogInSteps.cs. Тогда ваш основной файл определения этапов останется четким и ориентированным на уникальный сценарий, определяемый вами.

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

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

Брэндон Сэтром (Brandon Satrom) — разработчик-идеолог Microsoft в Остине, штат Техас. Ведет блог на userinexperience.com, а также его можно встретить в Twitter под учетной записью @BrandonSatrom.

Выражаю благодарность за рецензирование этой статьи экспертам: Пол Рейнер (Paul Rayner) и Кларк Селл (Clark Sell)