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

Не буду повторять сам себя, процитирую Фаулера:

That said, I do think that the greatest potential benefit of DSLs comes when business people participate directly in the writing of the DSL code. The sweet spot, however is in making DSLs business-readable rather than business-writeable. If business people are able to look at the DSL code and understand it, then we can build a deep and rich communication channel between software development and the underlying domain.

Наибольший потенциал DSL он видит в том, что текст на предметно-ориентированном языке могут писать и — главное — читать эксперты предметной области, а вовсе не программисты. Благодаря этому можно обеспечить совершенно другой уровень коммуникации между разработкой ПО и предметной областью.

В этом посте я хотел бы анонсировать библиотеку DelphiSpec. Не буду скрывать, что вдохновил меня проект Cucumber. То есть фактически библиотека является Delphi-реализацией языка Gherkin (переводится как «корнишон»). Реализация не полная (считайте альфа-версией). Некоторых возможностей пока нет, но проект будет развиваться. Мне он интересен.

Если в двух словах техническим языком выразить суть проекта, то DelphiSpec позволяет автоматически превратить подобный сценарий в тест для старого доброго DUnit:

Feature: Calculator

Scenario: Add two numbers
  Given I have entered 50 in calculator
    And I have entered 70 in calculator
  When I press Add
  Then the result should be 120 on the screen

Scenario: Add two numbers (fails)
  Given I have entered 50 in calculator
    And I have entered 50 in calculator
  When I press Add
  Then the result should be 120 on the screen

Scenario: Multiply three numbers
  Given I have entered 5 in calculator
    And I have entered 5 in calculator
    And I have entered 4 in calculator
  WHEN I press mul
  Then the result should be 100 on the screen

Подчеркну, это не тесты, это сценарии. То есть настоящий Behavior-Driven Development (BDD) в дополнение к известному Test-Driven Development (TDD). Такие сценарии могут использоваться как часть постановки задачи, как часть документации к проекту и конечно как тесты. С помощью сценариев не принято описывать внутренние особенности системы, сценарии описывают как происходящее выглядит для пользователя.

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

Вернемся к примеру с калькулятором. Результат выполнения сценариев в DUnit будет выглядеть так:
dunit-delphispec

Как это работает? Сам язык Gherkin очень мал. По сути единственная его функция — это описание связок Given-When-Then (по-русски: дано-если-то). Есть несколько ключевых слов, все остальное кастомизируется.

В примере мы тестируем очень простой класс-калькулятор. Здесь я его код выкладывать не буду, смотрите его по ссылке. Интересно другое: как программе понять и выполнить команды вроде «Given I have entered 50 in calculator»? Вот тут в дело вступает вспомогательный класс описывающий эти шаги (так называемые «step definitions»). Выглядит класс так:

unit SampleCalculatorStepDefs;

interface

uses
  SampleCalculator, DelphiSpec.Attributes, DelphiSpec.StepDefinitions;

type
  [_Feature('calculator')]
  TSampleCalculatorSteps = class(TStepDefinitions)
  private
    FCalc: TCalculator;
  public
    procedure SetUp; override;
    procedure TearDown; override;

    [_Given('I have entered (.*) in calculator')]
    procedure EnterInt(Value: string);

    [_When('I press Add')]
    procedure AddInt;

    [_When('I press Mul')]
    procedure MulInt;

    [_Then('the result should be (.*) on the screen')]
    procedure TestResult(Value: string);
  end;

implementation

uses
  System.SysUtils, TestFramework, DelphiSpec.Core;

{ TSampleCalculatorSteps }

procedure TSampleCalculatorSteps.AddInt;
begin
  FCalc.Add;
end;

procedure TSampleCalculatorSteps.EnterInt(Value: string);
begin
  FCalc.Push(Value.ToInteger);
end;

procedure TSampleCalculatorSteps.MulInt;
begin
  FCalc.Mul;
end;

procedure TSampleCalculatorSteps.SetUp;
begin
  FCalc := TCalculator.Create;
end;

procedure TSampleCalculatorSteps.TearDown;
begin
  FCalc.Free;
end;

procedure TSampleCalculatorSteps.TestResult(Value: string);
begin
  if FCalc.Value <> Value.ToInteger then
    raise ETestFailure.Create('Incorrect result on calculator screen');
end;

initialization
  RegisterStepDefinitionsClass(TSampleCalculatorSteps);

end.

То есть по сути у каждого шага есть тип (Given/When/Then) и регулярное выражение, описывающее остальную часть шага. Регулярные выражения помогают выделить из шагов параметры, которые будут переданы в методы класса для выполнения соответствующей части сценария.

Чтобы вся эта система заработала, остается только добавить в dpr-файл вызов процедуры, получающей на вход путь к файлам с описанием сценариев и превращающей эти сценарии в настоящие тесты для DUnit.

begin
  PrepareDelphiSpecs(TPath.Combine(ExtractFilePath(Application.ExeName), 'features'), True, 'EN');
  DUnitTestRunner.RunRegisteredTests;
end.

Последний параметр («EN») позволяет указать буквенный код языка, на котором написаны сценарии. Это еще одна интересная возможность Gherkin. Так как в общем-то нет разницы текст на каком языке обрабатывать с помощью регулярных выражений, то грех было не перевести на разные языки и несколько ключевых слов, для чего служит соответствующий XML-файл.

То есть ничто не мешает записывать сценарии даже вот так:

Функционал: Калькулятор

Сценарий: Сложить два числа
  Допустим я ввожу 50 на калькуляторе
         И я ввожу 70 на калькуляторе
  Когда я нажимаю СЛОЖИТЬ
  Тогда результат на экране должен быть 120

Это пока первые шаги, но постепенно я планирую реализовать все возможности Gherkin.

Связанные записи: