В данной статье я бы хотел поделиться с Вами идеей использования 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 пункт — это общие требования, они не зависят от конкретных правил распределения, которые имплементировал/имплементирует заказчик, поэтому подготовить тесты для проверки их выполнения можно заранее и прогонять их при изменения правил распределения. Я предлагаю делать это через формирование датасета — набора всех комбинаций возможных вариантов параметров. Строится он интуитивно понятно и благодаря текущей реализации 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
Таким образом все необходимые варианты для параметров можно получить из самих правил! А комбинации вариантов можно получить просто перемешиванием.
Правила, описываемые в 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.
MonkAlex
Таки у вас кто-то кроме разработчика реально так пишет?
Или всё равно разработчик добивает это до реально рабочего состояния?
Avvero Автор
В основном правила придумывают (на основе требований) и имплементируют только разработчики, но вместе с задачей иногда предоставляются и acceptance criteria в нотации Given-When-Then, а это уже полдела и остается только заимплементировать.
MonkAlex
Ну таки да, в этом вся печаль. Основная идея BDD то в первую очередь в том что требования пишут те, кому они ближе, а на деле всё равно разработчики.
Но, в любом случае интересный опыт, спасибо =)
Avvero Автор
Я наверное не правильно понял Ваш изначальный вопрос, описанное в статье решение — это proof of concept, рабочего и пользуемого приложения нет. Но идея как раз в том, что модифицировать правила должен именно «заказчик», а программист только бы расширял словарь «фраз»
ggo
Крайне суровые аналитики, конечно, могут писать BDD-тесты, но ввиду их относительной редкости, BDD-тесты чаще пишут разработчики/тестировщики в зависимости от особенностей команды. А BDD-тесты выступают человеко-понятными acceptance criteria с одной стороны, и машино-читаемыми интерфейсами к внутренностям тестов с другой стороны. И тогда BDD-тесты могут закрыть вопроса: А как работает сейчас (работало вчера, будет работать завтра — относительно простым человеческим языком)? И если тест упал, то как происходит декомпозиция в непосредственно реализацию набора действий?
vdshat
У нас реально тестировщики пишут сценари в BDD тестах (мы используем jBehaive).
А идея рулов на основе BDD сценариев интересная: человеческий язык всегда ближе. Вопрос только, как с теми же drools — производительность и надежность. BDD движки не расчитаны на производительную работу. Мы столкнулись с утечками памяти, проблемами в параллельной работе и т.п.
Avvero Автор
Вы конечно же правы, надежность cucumber при использовании в продакшене вызывает вопросы. Но внушает надежду тот факт, что имплементация cucumber (я видел конечно же только java) в принципе не шибко сложная — один только подход к разбору feature диво простой, поэтому шансы поправить что-то самостоятельно высоки.