Шаблон проектирования микросервисов в последние годы был широко принят и успешно внедрен многими крупными технологическими компаниями, включая Amazon и Netflix. Он решил множество сложных проблем, с которыми разработчики программного обеспечения сталкивались в корпоративных средах, использующих традиционные монолитные услуги. Разработка, управляемая поведением (BDD) - еще одна современная тенденция, появившаяся несколько лет назад из TDD, которая набирает обороты. Мартин Фаулер сказал: … BDD - это еще один вариант TDD, который обычно использует имитационное тестирование. Одно из главных преимуществ метода BDD заключается в том, что он создает более тесную связь между бизнес-аналитиками (BA) (также известными как владельцы продуктов, в терминологии Agile) и разработчиками программного обеспечения в течение жизненного цикла разработки.
Из-за снижения популярности. языка C # микросервисы, разработанные в коде C #, массово не производятся во всем мире. Стало сложной задачей найти исчерпывающий пример разработки микросервисов на языке C # с использованием подхода BDD. В этом руководстве описывается полный набор инструментов и пример кода для разработчиков C #, увлеченных BDD, для запуска проекта микросервиса BDD. Вместо того, чтобы использовать простой игрушечный сценарий, я использовал более практичный сценарный подход, чтобы лучше выразить цель использования BDD.

Практический сценарий

В настоящее время я работаю разработчиком программного обеспечения в страховой компании. Чтобы обеспечить лучший сервис и потребности наших застрахованных участников, компания недавно ввела многоуровневый план медицинского страхования. План разделил свои медицинские льготы на две категории, которые применимы к двум разным уровням доли затрат, то есть франшизам уровня 1 и 2 соответственно. Перед нашей группой Agile-разработчиков стоит задача разработать микросервис, который будет определять, достигли ли накопленные медицинские расходы участника франшизы каждого уровня и максимума наличных средств (OOP Max). Поскольку лишь несколько разработчиков в команде понимают, как работает многоуровневый план, владелец продукта подготовил для нас ряд историй. Следующие рассказы написаны на огурцовском языке.

Feature: MultiTierBenefitCostshare
 A new multi-tier benefit structure that covers
 Medical, dental & Rx major categories. It has 
 separate levels of cost share amount and PCP
 focused provider network
Scenario: Multi_tier medical benefit contains at least two levels of deductible according to number of tiers
 Given The medical benefit has level_one deductible and level_two deductible
 When I inquire the deductible amount
 Then the result should output level_one and level_two deductible
Scenario: Multi_tier medical benefit contains only one max OOP amount regardless number of tiers
 Given The medical benefit has only one max OOP amount
 When I inquire the max OOP amount
 Then the result should output one max OOP amount
Scenario: All tiers claim amount accumulation toward to one OOP amount
 Given The table below contains a sample of insured member medical claims for all tiers
 | MemberId | ProductId | Tier |     ClaimDesc    | Amount |
 | X0001    | ABC00001  | 1    | Office Visit     | 100.00 |
 | X0001    | ABC00001  | 1    | Blood Test       | 50.00  |
 | X0001    | ABC00001  | 2    | X-Ray            | 75.00  |
 | X0001    | ABC00001  | 2    | Specialist Visit | 150.00 |
 | X0002    | ABC00001  | 1    | Office Visit     | 150.00 |
 | X0002    | ABC00001  | 1    | Blood Test       | 125.00 |
 | X0002    | ABC00001  | 2    | X-Ray            | 75.00  |
 | X0002    | ABC00001  | 2    | Specialist Visit | 150.00 |
 | X0002    | ABC00001  | 2    | Tissue Removal   | 220.00 |
 And the max OOP amount is five hundred dollars
 When I inquire a member current OOP amount
 Then the result should be either a sum of claim amounts or its max OOP amount as the table below
 | MemberId | OopAmount |
 | X0001    | 375.00    |
 | X0002    | 500.00    |

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

Предварительная разработка

Как разработчику программного обеспечения, первая задача, которую мне нужно выполнить, - это приобрести набор инструментов разработки, которые можно использовать для BDD в нашем проекте. Ниже приведен список инструментов, которые я выбрал для разработки микросервиса на C #.

Необходимые инструменты:

  • Visual Studio 2017 Enterpeise Edition для Windows 10
  • Specflow для Visual Studio 2017
  • Пакет NuGet для Microsoft Entity Framework Core
  • Sqlite3 для пакета NuGet .Net Framework
  • Пакет Moq NuGet
  • Пакет NuGet Fluentassertions

Visual Studio 2017 Enterprise Edition - это стандартная среда для большинства разработчиков C #. подразумевает, что следуйте инструкциям мастера установки, чтобы завершить установку. Вы можете установить VS2017 Community или Professional Edition, если Enterprise Edition невозможно.

Specflow для Visual Studio 2017 можно установить с помощью диспетчера пакетов VS2017. В верхней строке меню войдите в диспетчер пакетов через Tools|Extensions and Updates... на рисунке ниже.

Остальные пакеты NuGet можно установить после создания проекта C #. Для вашего удобства вы можете просто загрузить или клонировать исходный код из папки csharp_demo моего репозитория Github и перестроить все решение. Решение автоматически установит для вас все необходимые пакеты NuGet.

Микросервисная архитектура:

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

Увеличивая масштаб предлагаемого микросервиса, я разделил микросервис на три основных объекта - объект репозитория для доступа к базе данных Plan Benefit, объект бизнес-логики для расчета франшизы и суммы ООП и объект контроллера для взаимодействия со шлюзом API. На диаграмме ниже представлены отношения между этими объектами.

«Шаги» развития

Существует множество форм создания микросервиса, таких как консольное приложение, веб-API или функция как услуга (FaaS) в облаке, например функция Azure или AWS Lambda. Поскольку Visual Studio 2017 уже предоставил готовый шаблон для создания веб-API, в этом руководстве мы создадим веб-API в качестве микросервиса. Чтобы создать веб-API, просто щелкните File->New->Project в строке меню VS2017 и выберите ASP.NET Web Application (.Net Framework) в качестве типа проекта. Затем щелкните значок «Пустой» и установите флажок «Веб-API», как показано ниже, чтобы создать приложение оболочки веб-API.

Очевидное различие между традиционным TDD и BDD состоит в том, что BDD связывает функциональные требования бизнеса, написанные здесь на языке Gherkin, с языком программирования. Чтобы активировать его привязку в коде C #, мы используем Specflow Techtalk в сочетании с платформой MSTest. Первым шагом является создание папки Features и папки Steps внутри вашего проекта модульного тестирования C #. Если вы клонировали / загрузили папку csharp_demo из моего репозитория Github microservice_bdd_demos, структура папок, показанная в обозревателе решений VS2017 ниже, является примером создания папок.

Затем добавьте новый элемент файла функций SpecFlow с расширением «.feature» в папку «Features», как показано ниже.

Затем скопируйте и вставьте содержимое предоставленного языка Gherkin в новый файл и щелкните правой кнопкой мыши в любом месте содержимого файла функции, чтобы выбрать Generate Step Definitions, как показано ниже. Вам будет предложено, какой код шага C # нужно сгенерировать автоматически и где сохранить файл .cs. Вам просто нужно просмотреть и выбрать только что созданную папку Шаги и нажать OK.

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

Файл MultiTierBenefitCostshare.feature.cs (который мы называем файлом «1») - это фактическая запись для модульного тестирования BDD, которую никогда не следует изменять. Приведенный ниже фрагмент кода будет автоматически сгенерирован для сценария «Многоступенчатое медицинское пособие содержит как минимум два уровня франшизы в зависимости от количества уровней», полученного из соответствующего файла функций.

[Microsoft.VisualStudio.TestTools.UnitTesting.TestMethodAttribute()]
[Microsoft.VisualStudio.TestTools.UnitTesting.DescriptionAttribute("Multi_tier medical benefit contains at least two levels of deductible according t" +
    "o number of tiers")]
[Microsoft.VisualStudio.TestTools.UnitTesting.TestPropertyAttribute("FeatureTitle", "MultiTierBenefitCostshare")]
public virtual void Multi_TierMedicalBenefitContainsAtLeastTwoLevelsOfDeductibleAccordingToNumberOfTiers()
{
    TechTalk.SpecFlow.ScenarioInfo scenarioInfo = new TechTalk.SpecFlow.ScenarioInfo("Multi_tier medical benefit contains at least two levels of deductible according t" +
            "o number of tiers", null, ((string[])(null)));
    this.ScenarioInitialize(scenarioInfo);
    this.ScenarioStart();
    testRunner.Given("The medical benefit has level_one deductible and level_two deductible", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Given ");
    testRunner.When("I inquire the deductible amount", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "When ");
    testRunner.Then("the result should output level_one and level_two deductible", ((string)(null)), ((TechTalk.SpecFlow.Table)(null)), "Then ");
    this.ScenarioCleanup();
}

Приведенный ниже фрагмент кода является частью автоматически сгенерированного файла MultiTierBenefitCostshareSteps.cs (именуемого файлом «2»), который будет выполняться файлом «1» во время модульного тестирования, и это будет наше целевое местоположение для выполнения BDD процесс. Возможно, вы быстро обнаружили, что файл «1», MultiTierBenefitCostshare.feature.cs, явно вызывает три атрибута: «Дано», «Когда» и «Тогда». , которые являются методами со стандартным порядком установки-действия-утверждения в методе модульного тестирования. По этой причине вы можете пропустить какие-либо действия с автоматически созданным файлом .feature.cs.

[Given(@"The medical benefit has level_one deductible and level_two deductible")]
public void GivenTheMedicalBenefitHasLevel_OneDeductibleAndLevel_TwoDeductible()
{
    ScenarioContext.Current.Pending();
}
[When(@"I inquire the deductible amount")]
public void WhenIInquireTheDeductibleAmount()
{
    ScenarioContext.Current.Pending();
}
[Then(@"the result should output level_one and level_two deductible")]
public void ThenTheResultShouldOutputLevel_OneAndLevel_TwoDeductible()
{
    ScenarioContext.Current.Pending();
}

В этом руководстве мы по-прежнему используем библиотеку Moq для имитации объектов во время установки и библиотеку FluentAssertions для утверждения. Теперь мы готовы запустить процесс BDD для всех трех шагов: «Дано», «Когда» и «Тогда» с стандартный цикл TDD - Красный, Зеленый и Рефакторинг на каждом этапе, как показано на диаграмме ниже.

«Данный» шаг

«Медицинское пособие включает франшизу level_one и франшизу level_two»

Красный: создайте объект контроллера как закрытый член, хотя мы еще не определили какой-либо класс контроллера с именем CostShareController().

[Binding]                                                                                                 
public class MultiTierBenefitCostshareSteps
{
   private CostShareController _controller = new CostShareController();

Теперь Intellisense VS2017 должен немедленно сообщить вам, что класса CostShareController не существует.

Зеленый: переместите указатель мыши в папку проекта веб-API, щелкните правой кнопкой мыши папку «Контроллер» и выберите «Класс контроллера веб-API (v2.1)». Он подскажет вам, какое имя класса контроллера вы хотите. Затем введите CostShareController и нажмите ОК, чтобы сгенерировать класс контроллера и файл C #.

Теперь давайте вернемся к ранее отредактированному коду C # [3] и введите using BenefitCsBdd.Controllers; в верхней части файла, и вы увидите, что Intellisense больше не жалуется. Поздравляю! Вы только что успешно создали свой первый объект с помощью BDD на C #.

Рефакторинг: поскольку автоматически сгенерированный код C # превратил три раздела модульного теста, Настройка, Действие и Утверждение, в рамках традиционного метода тестирования в три явных дочерних метода - «Дано», «Когда »И« Then » методы атрибута step, соответственно, мы также хотим обрабатывать каждый дочерний метод как обычный модульный тест. Давайте переместим создание объекта CostShareContoller в метод атрибута Given, как показано в приведенном ниже фрагменте кода C #. На этом завершается подцикл BDD «Дано».

public class MultiTierBenefitCostshareSteps
{
    public CostShareController _deductController = null;
    [Given(@"The medical benefit has level_one deductible and level_two deductible")]
    public void GivenTheMedicalBenefitHasLevel_OneDeductibleAndLevel_TwoDeductible()
    {
        _deductController = new CostShareController();    
    }

«Когда» Шаг

«Я запрашиваю франшизу

Красный. Основываясь на описании шага, мы можем интуитивно вызвать GetDeductible() метод из объекта CostShareController в качестве действия. Но мы быстро поняли, что должен быть параметр, позволяющий определить более точный результат на основе здравого смысла. Давайте использовать идентификатор участника в качестве параметра, как в приведенном ниже фрагменте кода.

[When(@"I inquire the deductible amount")]
public void WhenIInquireTheDeductibleAmount()
{
    deductController.GetDeductible("X0001");
}

Поскольку GetDeductible() метод еще не был определен в CostShareController class, Intellisese сразу же пожаловалась на него ..

Зеленый: давайте добавим GetDeductible(string memberId) метод к CostShareController классу, как показано ниже.

public class CostShareController : ApiController
{
    public void GetDeductible(string memberId)
    {
        return;
    }

Мы видим, что Intellisense сразу перестала жаловаться.

Рефакторинг. Поскольку контроллер MVC ASP.Net уже предопределил метод Get() как часть стандартных операций CRUD в своей таблице сопоставления маршрутизации, давайте переименуем метод GetDeductible() в Get (), чтобы он соответствовал. Кроме того, с точки зрения страхового бизнеса слово «франшиза» уже косвенно указывает на тип доли затрат. Слово «разделение затрат» встречается где-нибудь в коде C #, здесь стало ненужным. Итак, давайте переименуем класс CostShareController в класс DeductibleController, чтобы удалить избыточность, как показано ниже.

public class DeductibleController : ApiController
{
    public void Get(string memberId)
    {
        return;
    }

Теперь встроенный процесс рефакторинга VS2017 должен был автоматически обновить все операторы, относящиеся к объекту, соответственно.

public class MultiTierBenefitCostshareSteps
{
    public DeductibleController _deductController = null;

Давайте также изменим GetDeductible() метод на Get() в методе шага, чтобы прекратить жалобы от IntelliSense.

[When(@"I inquire the deductible amount")]
public void WhenIInquireTheDeductibleAmount()
{
    deductController.Get("X0001");
}

«Тогда» Шаг

«результат должен вывести уровень франшизы level_one и level_two»

Красный: на предыдущих этапах мы не определяли метод Get(), который бы возвращал что-либо, потому что в описании этого шага нам не говорилось об этом. Напротив, этот конкретный шаг показал, что метод Get() должен возвращать значение, которое содержит информацию о франшизе, такую ​​как уровень и сумма. Вернемся к предыдущему шагу «Когда» и заменим его на приведенный ниже код.

[When(@"I inquire the deductible amount")]
public void WhenIInquireTheDeductibleAmount()
{
    _deductibles = _deductController.Get("X0001");
}

И назначьте член класса public IEnumerable<Deductible> _deductibles = null; как коллекцию объекта франшизы и переменной члена класса MultiTierBenefitCostshareSteps.

Зеленый: поскольку сигнатура метода Get() в методе атрибута шага Когда не соответствует тому, что определено внутри класса DeductibleController, IntelliSense немедленно жалуется на это. Прежде чем мы попытаемся исправить проблему в объекте контроллера, давайте спроектируем объект POCO, который содержит информацию об уровне и сумме, как показано ниже, и сохраним ее в файл C # - Deductible.cs.

namespace BenefitCsBdd
{
    public class Deductible
    {
        public int Tier { get; set; }
        public decimal Amount { get; set; }
    }
}

Теперь переместим фокус на DeductibleController class и обновим метод Get(), как показано ниже.

public IEnumerable<Deductible> Get(string memberId)
{
    return null;
}

Поскольку мы пока не знаем никакой логики, как вычислить значения в объекте, подлежащем вычету, мы просто возвращаем нулевое значение для этого Get() метода. Вы можете ввести альтернативный оператор throw new NotImplementedException(); для этого метода, чтобы указать, что этот метод будет разработан позже, чтобы избежать возможной путаницы. На данный момент Intellisense вообще не должен ни на что жаловаться.

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

[When(@"I inquire the deductible amount")]
public void WhenIInquireTheDeductibleAmount()
{
    try
    {
        _deductible = _deductController.Get("X0001");
        _deductible.Should().NotBeNull();
    }
    catch (Exception e)
    {
        e.Message.Should().BeNullOrEmpty();
    }
}

Теперь мы можем запустить модульный тест для этого конкретного сценария, щелкнув правой кнопкой мыши соответствующий файл кода C # и выбрав Run Selected Tests, как показано ниже.

Красный: результат выполнения вышеуказанного теста показывает, что тест не прошел. Если вы отлаживаете и проходите через сам код модульного теста, вы обнаружите, что он терпит неудачу при утверждении внутри метода шага атрибута Then. Поскольку на самом деле мы не реализовали никакой логики получения вычитаемых значений, ожидается, что модульный тест будет провален. В реальном случае вы должны немедленно связаться с вашим заказчиком / заказчиком, чтобы добавить фактическую логику к методу Get(). В качестве учебного пособия давайте просто создадим имитацию объекта и продолжим.

Зеленый: чтобы создать фиктивный объект с помощью библиотеки Moq, нам нужно сначала создать интерфейс как часть шаблона внедрения зависимостей (DI). Назовем интерфейс IBenefit, создадим соответствующий файл .cs и добавим в него следующий код.

using System;
using System.Collections.Generic;
namespace BenefitCsBdd
{
    public interface IBenefit
    {
        IEnumerable<Deductible> GetDeductible(string productId);
    }
}

Теперь вернемся к методу атрибута step «Given» и изменим код следующим образом.

public void GivenTheMedicalBenefitHasLevel_OneDeductibleAndLevel_TwoDeductible()
{
    var deductibles = new List<Deductible>()
    {
        new Deductible() {Tier = 1, Amount = 500 },
        new Deductible() {Tier = 2, Amount = 1000 }
    };
    var mockBenefit = new Mock<IBenefit>();
    mockBenefit.Setup(deduct => deduct.GetDeductible("X0001")).Returns(deductibles);
_deductController = new DeductibleController(mockBenefit.Object);
}

Поскольку класс DeductibleController изначально не был создан с каким-либо конструктором, принимающим параметры, Intellisense сразу же жалуется на неправильную инициализацию объекта. Вернемся к классу DeductibleController и добавим новый конструктор, который может принимать объект в качестве параметра, производного от интерфейса IBenefit, как показано ниже.

public class DeductibleController : ApiController
{
    private readonly IBenefit _benefit;
    public DeductibleController(IBenefit benefit)
    {
        _benefit = benefit;
    }
    public IEnumerable<Deductible> Get(string memberId)
    {
        return _benefit.GetDeductible(memberId);
    }

А теперь давайте снова запустим тест - он прошел!

Рефакторинг. Пришло время определить конкретный объект с именем «MultiTierBenefit», который является производным от интерфейса IBenefit и сохранить его в файле C # с именем BenefitCsBdd.cs, как показано ниже.

using System;
using System.Collections.Generic;
using System.Linq;
namespace BenefitCsBdd
{
    public class MultiTierBenefit : IBenefit
    {
        public IEnumerable<Deductible> GetDeductible(string memberId)
        {
            return null;
        }
    }
}

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

Резюме

Возможно, вы уже заметили, что процесс BDD в этом руководстве по-прежнему соответствует традиционному стандартному процессу TDD, точно так же, как показано на диаграмме процесса BDD в этой статье - шаги Give-When-Then, включая, возможно, несколько итераций цикла Red-Green-Refactoring в рамках каждый шаг. Что касается микросервиса, это руководство может не касаться создания микросервисов, но оно касается самого важного аспекта микросервиса - «делать только одно и ничего больше». Мы предположили, что PO / BA уже выполнил хорошее разделение доменов, также называемое ограниченным контекстом, анализ и хорошо описал его в файле функций. Как разработчику микросервисов, нам просто нужно создать веб-API, опять же, что является одной из многих форм микросервисов, с нашим самым хорошо осведомленным языком программирования и инструментами, следуя сценариям, описанным в файле функций, общаться с PO / BA, когда он необходимо и протестировать. Желательно, чтобы мы все могли создать конвейер CI / CD для доставки микросервисов в стиле Agile.

Заключительные примечания

Окончательный код содержит два современных компонента, которые очень помогли процессу разработки: Sqlite3 Database и Entity Framework Core 2.2.

База данных Sqlite3: Sqlite3 - это облегченная база данных, которую можно настроить как локальный файл или базу данных в памяти. Его объем памяти настолько незначителен, что его можно физически разместить в контейнере Docker без значительного влияния на производительность микросервиса. Несмотря на то, что это не полностью функциональная база данных, такая как база данных SQL Server или PostgreSql, ее опция базы данных в памяти действительно предоставляет отличный способ повысить тестируемость операций с базой данных, таких как запрос и обновление, без использования фиктивного объекта.

Entity Framework Core: Entity Framework - самый популярный инструмент объектно-реляционного сопоставления (ORM) для программирования, связанного с реляционными базами данных, на C #, особенно для стиля разработки, ориентированного на код. Эта версия совместима как с .Net Framework, так и с .Net Core CLR. Он значительно упрощает разработку кода для стандартных операций CRUD и предлагает множество сторонних подключаемых модулей баз данных, таких как Sqlite, PostgresSql и SQL server. Плагины можно заменять путем настройки без изменения разработанного кода C #. Как и в этом руководстве, объект репозитория настроен для работы с базой данных Sqlite3 в памяти во время модульного тестирования и PostgresSql во время фактического использования в производственной среде.