Использование cucumber в качестве движка бизнес правил +12


В данной статье я бы хотел поделиться с Вами идеей использования cucumber в качестве движка бизнес-правил и подходом к проверке таких правил.


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


  • для клиента должна быть выбрана группа согласно установленным правилам распределения
  • для каждого клиента должна быть выбрана только одна группа

Клиенту могут быть присущи такие параметры: страна, идентификатор, язык и т.д.


Cucumber is a tool that supports Behaviour-Driven Development (BDD) — a software development process that aims to enhance software quality and reduce maintenance costs.
Gherkin is a Business Readable, Domain Specific Language that lets you describe software's behaviour without detailing how that behaviour is implemented.


Причины


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


Причина 1


Во многих проектах, где я принимаю участие, используется drools. Правила строго покрываются unit тестами, а кое где даже используются BDD (Behaviour Driven Development) тесты с помощью cucumber. В одном из таких проектов я заметил странную штуку — код BDD тестов сильно походит на код drools правил.
Приведу пример. Код на drools выглядит так:


rule "For client from country ENG group will be England"
when
    Client(country == "ENG")
then
    insert(new Group("England"));
end

Описание фичи (cucumber feature на языке gherkin) в BDD тесте выглядит так:


  Scenario: For client from country ENG group will be England
     When client's country is "ENG"
     Then group will be "England"

Если абстрагироваться от синтаксиса — один в один! Пример по большей части синтетический, но тем не менее он честный — в реальном проекте все так и сложилось. По факту, наши ожидания описаны в двух местах двумя разными способами и кажется, что второй (gherkin) явно человекопонятнее. Так почему бы сразу не писать на нем?


Причина 2


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


Как я упомянул выше, мы пользуем drools и любим его (хотя это наверное уже не те романтические чувства, а просто привычка), и для решения озвученной проблемы в экосистеме drools имеется инструмент — drools workbench, который обладает богатым (и главное достаточным) функционалом. Но мне кажется, что давать заказчику этот инструмент "как есть" и пусть он пишет правила как му заблагорассудится — это сильно оптимистичная идея. Потому что drools — штука сложная и нужно быть как минимум специально обученным, чтобы в него суметь. При этом покрывать drools-правила тестами строго обязательно — это конечно мое мнение, но я его всем навязываю.
Для упрощения работы с drools workbench можно воспользоваться следующим:


  • реализовать dsl с упрощенным синтаксисом
  • использовать disicion tables

При этом таблицы — это другой уровень абстракции и больше конфигурация, чем имплементации через бизнес правила.
Drools dsl позволяет сделать так, что вместо


when
    Client(country == "ENG")
then
    insert(new Group("England"));
end

можно написать так


when
    client's country is "ENG"
then
    group will be "England"
end

Dsl — это правильный путь и он лежит в ту степь, где уже пасется cucumber с своим gherkin.


Причина 3


Данная причина связана с проблемой тестирования — не понятно, как заказчик будет тестировать правила, написанные собственноручно. Идеально — писать тесты, но я в это верю еще меньше, чем в заказчика, пишущего на drools. Минимум, что нужно сделать в этом случае программисту — предоставить общие тесты базовой логики. Например для нашего приложения описаны требования:


  • для клиента должна быть выбрана группа согласно установленным правилам распределения
  • для каждого клиента должна быть выбрана только одна группа

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


  • получить все возможные варианты значений параметров (страна клиента, язык и т.д.)
  • построить набор всех комбинаций возможных вариантов параметров
  • расчитать для каждой комбинации группу
  • проверить выполнение условий

Реализация


Сucumber не заточен под использование вне тестовой среды, но уговорами и угрозами удалось заставить его работать. Пример proof of concept можно посмотреть тут https://github.com/avvero/crools. Собственно, cucumber в качестве движка бизнес правил использовать можно. That's all Folks!


Тестирование


Для нашего приложения нужно протестировать следующее:


  1. для клиента должна быть выбрана группа согласно установленным правилам распределения
  2. для каждого клиента должна быть выбрана только одна группа
  3. для набора параметров А выбирается группа Б

При этом 1 и 2 пункт — это общие требования, они не зависят от конкретных правил распределения, которые имплементировал/имплементирует заказчик, поэтому подготовить тесты для проверки их выполнения можно заранее и прогонять их при изменения правил распределения. Я предлагаю делать это через формирование датасета — набора всех комбинаций возможных вариантов параметров. Строится он интуитивно понятно и благодаря текущей реализации cucumber довольно просто. Почему интуитивно понятно? Приведу пример.


Если взять условие:


client's country is 'RUS'

, то я бы написал unit-тест для таких значений страны:


  • null
  • RUS
  • undefined

Если взять условия:


client's country is 'RUS'
client's country is 'CHL'

, то я бы написал unit-тест для таких значений страны:


  • null
  • RUS
  • CHL
  • undefined

Если взять условие:


client's payment > '1000'

, то я бы написал unit-тест для таких значений:


  • null
  • 0
  • 999
  • 1000
  • 1001
  • -1000

Таким образом все необходимые варианты для параметров можно получить из самих правил! А комбинации вариантов можно получить просто перемешиванием.


Для тех, кто не знаком с cucumber

Правила, описываемые в feature файле (на языке gherkin), должны быть поддержаны в definition файле (я использую java реализацию). Поддержка в данном случае — это возможность сопоставить (через регулярку) запись из feature файла методу из definition файла, иначе запись будет не понятна и не будет обработана (либо ошибка, либо игнорирование целого правила). Например для


  Scenario: For client from country ENG group will be England
     When client's country is "ENG"
     Then group will be "England"

должны быть описаны методы


    private Set<String> groups = new HashSet<>();

    @When("^client country is \"([^\"]*)\"$")
    public void clientCountryIs(String code) throws Throwable {
        Assert.isTrue(code.equals(client.getCountry()));
    }
    @Then("^group will be \"([^\"]*)\"$")
    public void groupWillBe(String code) throws Throwable {
        groups.add(code);
    }

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


Получение вариантов параметров можно реализовать путем описания дополнительного definition файла с реализацией методов таким образом


    @When("^client's country is \"([^\"]*)\"$")
    public void clientCountryIs(String code) throws Throwable {
        factDictionary.getCountries().add(null);
        factDictionary.getCountries().add(code);
        factDictionary.getCountries().add(UNDEFINED);
    }

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


Давайте разберем пару примеров такого тестирования (можно самостоятельно через демку http://avvero.pw/crools/)


Пример 1


Feature: Select group
  Scenario: England
     When client country is "ENG"
     Then group will be "England"

Результат проверки


Some entries have not been distributed:
#0 {"client":{},"deposit":{}}
#1 {"client":{"country":"any"},"deposit":{}}

Значит нужно добавить еще одно правило


Feature: Select group
  Scenario: England
     When client country is "ENG"
     Then group will be "England"
  Scenario: Default
     When client country is not "ENG"
     Then group will be "Default"

Пример 2


Feature: Select group
  Scenario: England
     When client country is "ENG"
     Then group will be "England"
  Scenario: Russia
     When client country is "RUS"
     Then group will be "Russia"
  Scenario: RichRussia
     When client country is "RUS"
     And deposit >= 1000
     Then group will be "RichRussia"

Результат проверки


Some entries have not been distributed
#0 {"client":{"country":"any"},"deposit":{"amount":999}}
#1 {"client":{},"deposit":{"amount":1001}}
#2 {"client":{"country":"any"},"deposit":{"amount":1001}}
...
Some entries have been distributed to more than one group
{"client":{"country":"RUS"}, {"amount":1001}}: Russia, RichRussia

Исправим проблему с попаданием в более чем одну группу добавив условие And deposit < 1000


  Scenario: Russia (no deposit)
     When client country is "RUS"
     And deposit is null
     Then group will be "Russia"
  Scenario: Russia
     When client country is "RUS"
     And deposit < 1000
     Then group will be "Russia"

А проблему с отсутствием правил для части вариантов таким правилом


   Scenario: Default
     When client country not in
     |ENG|
     |RUS|
     Then group will be "Default"

Получается, что "хороший" набор правил будет выглядеть так


 Feature: Select group
  Scenario: England
     When client country is "ENG"
     Then group will be "England"
  Scenario: Russia (no deposit)
     When client country is "RUS"
     And deposit is null
     Then group will be "Russia"
  Scenario: Russia
     When client country is "RUS"
     And deposit < 1000
     Then group will be "Russia"
  Scenario: RichRussia
     When client country is "RUS"
     And deposit >= 1000
     Then group will be "RichRussia"
  Scenario: Default
     When client country not in
     |ENG|
     |RUS|
     Then group will be "Default"

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


3 для набора параметров А выбирается группа Б

, то тут придется все-таки смотреть глазами — правда ли нужные группы заполнены клиентами с правильными параметрами. Но благодаря датасету это можно сделать легко и нет необходимости проходится по чеклисту и руками проверять варианты.


В зависимости от требований и прочих условий (типа "я могу предположить возможные сценарии использования заказчиком") можно добавить и другие проверки на основе датасета.
Проверки типа "а точно ли клиент из России с депозитом 2000 попадет в группу RichRussia?" можно осуществить так — посмотреть (предварительно предоставив GUI для такого дела) какие клиенты попали в RichRussia или куда попали клиенты из России с депозитом больше 1000.




К сожалению, не доступен сервер mySQL