Ещё раз про DelphiSpec (http://roman.yankovsky.me/?p=1258) и "тестировании по-русски" (http://18delphi.blogspot.ru/2013/11/gui.html).
Побудительным мотивом данного поста послужил комментарий Николая Зверева - "Это действительно круто, наконец-то от требований к тестам всего один шаг — формализовать требования в виде сценария… (ну и со стороны программы предоставить API)".
Тут я понял, что "я не всё правильно рассказал про "свои скрипты"" и что "Роман уделал меня на моём же поле" :-)
Я не претендую на первенство или оригинальность.
Тем более я не хочу умалять достоинства разработки Романа или критиковать её.
Я лишь хочу подкинуть "пищу для размышлений".
Итак - "как я бы делал бы "то же самое" на своей скриптовой машине".
Давайте возьмём пример Романа:
И "адаптируем" его к нашей скриптовой машине.
Первым делом посмотрим на класс:
Отобразим его (на стороне Delphi) на аксиоматику тестовой машины.
Это можно сделать несколькими способами:
1. Ручное отображение, через RegisterMethod.
2. UML и кодогенерацию.
3. "Новый" RTTI.
Я пользуюсь ВСЕМИ этими способами, в зависимости от задачи и её сложности.
Но это в общем - "не сильно важно".
Я лишь хочу наметить "пути".
Если кому-то интересен код "отображения" - я им потом поделюсь.
В результате отображения получим следующие слова скриптовой машины:
Теперь опишем аксиоматику на стороне скриптов:
Отдельно - базовую аксиоматику (она - ОДНА "на всех", это можно ОДИН РАЗ написать и "забыть как страшный сон"):
Далее регистрируем слова-"заглушки", семантика которых не влияет на код сценария.
Если я всё правильно понял - они служат лишь "декорацией".
(Роман наверное меня поправит, если я что-то не так понял)
И ещё определяем слово Then.
Семантика которого понятна - проверить условие и вывести ошибку, если условие не выполняется.
И отдельно аксиоматику калькулятора:
Тогда пример переписывается вот так:
Выглядит наверное "тяжеловесно" для начала.
Но!
Это практически повторяет реализацию Романа, но при этом обладает - гораздо большей гибкостью.
Это - раз.
Два - это то, что код скриптов не требует перекомпиляции приложения.
А три - это то, что классы в "аксиоматику" можно отображать "один в один" (разными способами, я часть из них перечислил выше), а "синтаксический сахар" - можно "добирать" уже средствами скриптов.
А четыре - скрипты - КОМПИЛИРУЮТСЯ, ДО их выполнения. С указанием места ошибки. В отличии от регулярных выражений.
Повод для размышлений?
Побудительным мотивом данного поста послужил комментарий Николая Зверева - "Это действительно круто, наконец-то от требований к тестам всего один шаг — формализовать требования в виде сценария… (ну и со стороны программы предоставить 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
 
И это тоже круто :)
ОтветитьУдалитьВообще идея тестирования, на базе неких user-friendly скриптов (а не, так сказать, hard-code) - круто. Очень-мега-круто.
Потому что тестирование и программирование - это всё-таки разные вещи. Конечно, есть люди, которые могут в себе сочетать несколько направлений деятельности - и БД проектировать, и API к этой БД, и web, использующий этот API, и Delphi-клиент, использующий это API, и ещё тесты (и того, и другого и третьего)... но это и редкость, и не есть хорошо в принципе.
Сегодня я больше склоняюсь к принципу "разделяй и властвуй". Т.е.: "ты пишешь код (который предоставляет описанное API), а ты пишешь скрипт (который тестирует этот код по описанному API)". ..как-то так..
"Потому что тестирование и программирование - это всё-таки разные вещи."
УдалитьТак я вроде и написал про "слои"...
А если, при этом, код и тесты формируются автоматически по модели.. эхъ.
ОтветитьУдалить"А если, при этом, код и тесты формируются автоматически по модели.. эхъ."
УдалитьТак и есть :-) И ТЗ кстати - тоже...
Интересно, но по-моему более трудоемко, чем использование DelphiSpec (забудем, что она не доделана еще, поговорим абстрактно)... хотя безусловно гибче. Вопрос: а нужна ли такая гибкость? Цель Gherkin - избавить людей от программирования, а избавление от программирования и большая гибкость плохо сочетаются.
ОтветитьУдалитьНо интересная вещица, безусловно. Я, признаюсь, только теперь кажется окончательно понял, как оно у тебя устроено :)
"Я, признаюсь, только теперь кажется окончательно понял, как оно у тебя устроено :)"
УдалитьНу слава богу :-) не прошло и года - как я смог кому-нибудь что-нибудь объяснить :-) я тот ещё "объяснятель"..
я знал что ты про трудоёмкость скажешь :-)
ОтветитьУдалитьповерь - тебе только так КАЖЕТСЯ
" а избавление от программирования и большая гибкость плохо сочетаются."
ОтветитьУдалитьВроде всё сочетается. Я же написал, что есть "слои". проектные классы -> API - > обвязка -> Собственно скрипты