пятница, 20 декабря 2013 г.

Ещё раз про DelphiSpec и "тестировании по-русски"

Ещё раз про DelphiSpec (http://roman.yankovsky.me/?p=1258) и "тестировании по-русски" (http://18delphi.blogspot.ru/2013/11/gui.html).

Побудительным мотивом данного поста послужил комментарий Николая Зверева - "Это действительно круто, наконец-то от требований к тестам всего один шаг — формализовать требования в виде сценария… (ну и со стороны программы предоставить API)".

Тут я понял, что "я не всё правильно рассказал про "свои скрипты"" и что "Роман уделал меня на моём же поле" :-)

Я не претендую на первенство или оригинальность.

Тем более я не хочу умалять достоинства разработки Романа или критиковать её.

Я лишь хочу подкинуть "пищу для размышлений".

Итак - "как я бы делал бы "то же самое" на своей скриптовой машине".

Давайте возьмём пример Романа:

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

И "адаптируем" его к нашей скриптовой машине.

Первым делом посмотрим на класс:

type
  TCalculator = class
  private
    FData: TStack<integer>;
    FValue: Integer;
  public
    constructor Create;
    destructor Destroy; override;

    procedure Add;
    procedure Mul;
    procedure Push(Value: Integer);

    property Value: Integer read FValue;
  end;

Отобразим его (на стороне Delphi) на аксиоматику тестовой машины.

Это можно сделать несколькими способами:

1. Ручное отображение, через RegisterMethod.
2. UML и кодогенерацию.
3. "Новый" RTTI.

Я пользуюсь ВСЕМИ этими способами, в зависимости от задачи и её сложности.

Но это в общем - "не сильно важно".

Я лишь хочу наметить "пути".

Если кому-то интересен код "отображения" - я им потом поделюсь.

В результате отображения получим следующие слова скриптовой машины:

 
 OBJECT: TCalculator FUNCTION TCalculator.Create
 PROCEDURE TCalculator.Add OBJECT: TCalculator IN aCalculator
 PROCEDURE TCalculator.Mul OBJECT: TCalculator IN aCalculator
 PROCEDURE TCalculator.Push INTEGER IN aValue OBJECT: TCalculator IN aCalculator
 INTEGER FUNCTION TCalculator.GetValue OBJECT: TCalculator IN aCalculator

Теперь опишем аксиоматику на стороне скриптов:

Отдельно - базовую аксиоматику (она - ОДНА "на всех", это можно ОДИН РАЗ написать и "забыть как страшный сон"):

[WordWorker2] 
// - [WordWorker2] это слово, которое выполняется при компиляции кода и принимает ДВА ПАРАМЕТРА СПРАВА 
 Feature:

  FUNCTOR IN aClassFactory
  // - aClassFactory - фабрика тестируемого класса
  aClassFactory := WordToWork1 

  [] VAR l_Steps
  // - объявляем массив сценариев
  l_Steps := ( [[ WordToWork2 DO ]] )
  // - складываем сценарии в массив
  @ 
  ( 
    FUNCTOR IN aStep 
    // - aStep - текущий сценарий
    aClassFactory aStep DO 
  ) ITERATE l_Steps
  // - регистрируем сценарии в тестовой машине
; // Feature:

OBJECT VAR gTestedObject
// - текущий тестируемый объект

WordWorker2 Scenario: FUNCTOR IN anObjectConstructor
  STRING VAR l_ScenarioName
  // - объявляем переменную для имени сценария
  l_ScenarioName := ( WordToWork1 DO )
  // - получаем имя сценария
  l_ScenarioName 
  // - кладём имя сценария на стек
  @ (
   gTestedObject := ( 
    [ 
    // - Обратно переключаемся в режим выполнения
    anObjectConstructor CompileValue
    // - компилируем значение anObjectConstructor как "литерал" в коде
    ]
    // - Обратно переключаемся в режим компиляции 
    DO 
   )
   // - создаём тестируемый объект
   TRY
    [ WordToWork2 CompileValue ] DO
    // - выполняем код сценария
   FINALLY
    gTestedObject TObject.Free
    // - уничтожаем тестируемый объект
   END
  ) 
  // - компилируем код сценария на стек
  TestEngine.RegisterTest
  // - регистрируем в тестовой машине тест с именем сценария и указанным кодом
; // Scenario:

Далее регистрируем слова-"заглушки", семантика которых не влияет на код сценария.

Если я всё правильно понял - они служат лишь "декорацией".

(Роман наверное меня поправит, если я что-то не так понял)

WordWorker Given
 WordtoWork DO
; // Given

WordWorker And
 WordtoWork DO
; // And

WordWorker When
 WordtoWork DO
; // When

И ещё определяем слово Then.

Семантика которого понятна - проверить условие и вывести ошибку, если условие не выполняется.
WordWorker Then
 // - ожидает, что параметр справа вычислит массив из ДВУХ значений - булевского и строкового
 [] VAR l_Check
 // - объявляем массив
 l_Check := ( WordtoWork DO )
 // - присваиваем массиву значение
 BOOLEAN VAR l_Condition
 // - условие
 l_Condition := ( l_Check [0] )
 if ( NOT l_Condition ) then
 ( 
  Fail ( l_Check [1] )
  // - выводим сообщение о неуспехе
 )
; // Then

И отдельно аксиоматику калькулятора:

OBJECT FUNCTON Calculator
 Resut := ( @ TCalculator.Create )
 // - возвращаем указатель на конструктор объекта
;

PROCEDURE "I have entered {(INTEGER IN aValue)} in calculator"
 aValue gTestedObject TCalculator.Push
;

PROCEDURE "I press Add"
 gTestedObject TCalculator.Add
;

PROCEDURE "I press mul"
 gTestedObject TCalculator.Mul
;

[] FUNCTION "the result should be {(INTEGER IN aValue)} on the screen"
 Result := 
  [[  
   ( gTestedObject TCalculator.GetValue = aValue ) 
   'Incorrect result on calculator screen' 
  ]]
;

Тогда пример переписывается вот так:

 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"
  )
 )

Выглядит наверное "тяжеловесно" для начала.

Но!

Это практически повторяет реализацию Романа, но при этом обладает - гораздо большей гибкостью.

Это - раз.

Два - это то, что код скриптов не требует перекомпиляции приложения.

А три - это то, что классы в "аксиоматику" можно отображать "один в один" (разными способами, я часть из них перечислил выше), а "синтаксический сахар" - можно "добирать" уже средствами скриптов.

А четыре - скрипты - КОМПИЛИРУЮТСЯ, ДО их выполнения. С указанием места ошибки. В отличии от регулярных выражений.

Повод для размышлений?

P.S. Продолжение темы - http://programmingmindstream.blogspot.ru/2013/12/delphispec-2.html

8 комментариев:

  1. И это тоже круто :)
    Вообще идея тестирования, на базе неких user-friendly скриптов (а не, так сказать, hard-code) - круто. Очень-мега-круто.

    Потому что тестирование и программирование - это всё-таки разные вещи. Конечно, есть люди, которые могут в себе сочетать несколько направлений деятельности - и БД проектировать, и API к этой БД, и web, использующий этот API, и Delphi-клиент, использующий это API, и ещё тесты (и того, и другого и третьего)... но это и редкость, и не есть хорошо в принципе.
    Сегодня я больше склоняюсь к принципу "разделяй и властвуй". Т.е.: "ты пишешь код (который предоставляет описанное API), а ты пишешь скрипт (который тестирует этот код по описанному API)". ..как-то так..

    ОтветитьУдалить
    Ответы
    1. "Потому что тестирование и программирование - это всё-таки разные вещи."

      Так я вроде и написал про "слои"...

      Удалить
  2. А если, при этом, код и тесты формируются автоматически по модели.. эхъ.

    ОтветитьУдалить
    Ответы
    1. "А если, при этом, код и тесты формируются автоматически по модели.. эхъ."

      Так и есть :-) И ТЗ кстати - тоже...

      Удалить
  3. Интересно, но по-моему более трудоемко, чем использование DelphiSpec (забудем, что она не доделана еще, поговорим абстрактно)... хотя безусловно гибче. Вопрос: а нужна ли такая гибкость? Цель Gherkin - избавить людей от программирования, а избавление от программирования и большая гибкость плохо сочетаются.

    Но интересная вещица, безусловно. Я, признаюсь, только теперь кажется окончательно понял, как оно у тебя устроено :)

    ОтветитьУдалить
    Ответы
    1. "Я, признаюсь, только теперь кажется окончательно понял, как оно у тебя устроено :)"

      Ну слава богу :-) не прошло и года - как я смог кому-нибудь что-нибудь объяснить :-) я тот ещё "объяснятель"..

      Удалить
  4. я знал что ты про трудоёмкость скажешь :-)
    поверь - тебе только так КАЖЕТСЯ

    ОтветитьУдалить
  5. " а избавление от программирования и большая гибкость плохо сочетаются."
    Вроде всё сочетается. Я же написал, что есть "слои". проектные классы -> API - > обвязка -> Собственно скрипты

    ОтветитьУдалить