понедельник, 2 июня 2014 г.

Тестируем калькулятор № 6.2. Тестирование с использованием псевдослучайных данных

Имея в распоряжении 4 теста для 4 операций мы точно не проверяем "большой разброс" во входных данных. Для того чтобы расширить наше тестовое покрытие мы переходим к нашей новой главе - "Тестированию с использованием псевдослучайных данных". Как следует из названия, нам необходим какой-то случайный набор данных для тестирования. Результаты тестирования при этом будут записываться в выходной файл, так же как мы делали это в прошлой главе. 

Для начала проведем небольшой рефакторинг нашего класса TCalculatorOperationViaEtalonTest
Введем "тип функции" TCalcOperation
  
type
  TCalcOperation = function (const A, B: string): string of object;

Перепишем функцию AddArgumentsToLog и добавим её в класс TCalculatorOperationViaEtalonTest
procedure TCalculatorOperationViaEtalonTest.CheckOperation(
                                                    aLogger: TLogger;
                                                    aX1, aX2: string;
                                                    anOperation : TCalcOperation);
begin
  aLogger.OpenTest(Self);
  aLogger.ToLog(aX1);
  aLogger.ToLog(aX2);
  aLogger.ToLog(anOperation(aX1,aX2));
  CheckTrue(aLogger.CheckWithEtalon);
end;
Теперь наши тесты приобрели вид:
procedure TCalculatorOperationViaEtalonTest.TestDiv;
var
  x1, x2 : string;
begin
  x1:= cA;
  x2:= cB;

  CheckOperation(g_Logger, x1, x2, TCalculator.Divide);
end;

Далее выделим новый класс, который будет отвечать за "тесты с псевдослучайными данными" TCalculatorOperationRandomSequenceTest.
 type
  TCalcOperation = function (const A, B: string): string of object;

  TCalculatorOperationRandomSequenceTest = class(TTestCase)
   private
    procedure CheckOperation(aLogger: TLogger;
                             aX1, aX2: Double;
                             anOperation : TCalcOperation);
    procedure CheckOperationSeq(aLogger: TLogger;
                                anOperation : TCalcOperation);
   published
    procedure TestDiv;
    procedure TestMul;
    procedure TestAdd;
    procedure TestSub;
  end;//TCalculatorOperationRandomSequenceTest

У нас появилась новая процедура CheckOperationSeq, которая "отобрала" часть функциональности CheckOperation, а именно:
procedure TCalculatorOperationRandomSequenceTest.CheckOperationSeq(
  aLogger: TLogger;
  anOperation: TCalcOperation);
begin
  aLogger.OpenTest(Self);
  CheckOperation(aLogger, 5, 10, anOperation);
  CheckTrue(aLogger.CheckWithEtalon);
end;


procedure TCalculatorOperationRandomSequenceTest.CheckOperation(
  aLogger: TLogger;
  aX1, aX2: Double;
  anOperation : TCalcOperation);
begin
  aLogger.ToLog(aX1);
  aLogger.ToLog(aX2);
  aLogger.ToLog(anOperation(FloatToStr(aX1),FloatToStr(aX2)));
end;

Как видим для тестирования необходимо вызывать CheckOperationSeq которая уже в свою очередь вызовет CheckOperation с теми параметрами которые мы укажем при запуске. При этом мы дважды передаем необходимую anOperation : TCalcOperation функцию для вызова. Следующим шагом "перегрузим" процедуру записи в файл, так чтобы она "понимала" Double:
...
   procedure ToLog(const aParametr: Double); overload;
...

procedure TLogger.ToLog(const aParametr: Double);
begin
  Writeln(FTestFile, FloatToStr(aParametr) + ' ');
end;

Последним шагом, ради которого мы и затевали все предыдущие изменения мы изменяем нашу процедуру "проверки последовательности"(CheckOperationSeq), так чтобы она проверяла случайные аргументы. При этом как мы видим второй аргумент равен 2000 * Random + 1, единица добавляется, для того чтобы у нас не возникло случайного деления на 0. Однако вопрос с обработкой исключений при тестировании мы затронем в будущих статьях.

Отдельного упоминания заслуживает первая строчка в процедуре RandSeed := 40000; - таким образом мы фиксируем "Random" так что наша последовательность всегда одинакова. Нужно нам это для того чтобы каждый раз не "переливать" наши эталоны.
procedure TCalculatorOperationRandomSequenceTest.CheckOperationSeq(
  aLogger: TLogger;
  anOperation: TCalcOperation);
var
  l_Index : Integer;
begin
  RandSeed := 40000;
  aLogger.OpenTest(Self);
  for l_Index := 0 to 10000 do
    CheckOperation(aLogger,
                   1000 * Random,
                   2000 * Random + 1, anOperation);
  CheckTrue(aLogger.CheckWithEtalon);
end;

Теперь для каждой операции будет осуществляться 10к вариантов тестирования, в принципе можно и больше, всё зависит от мощностей железа на котором мы запускаем тесты. На этом моменте в принципе можно было бы подводить итоги, однако после того как Александр слил себе исходники, обнаружилась проблема региональных настроек компьютера, а именно у Александра десятичные числа, записывались с запятой. А у меня с точкой. Всё бы ничего, однако наши "эталоны" уже слиты в гит с точкой. Проблему Александр "залатал на скорую руку" с помощью нового метода класса TCalculator:
class function TCalculator.FloatToStr(aValue: Double): string;
var
 l_FS : TFormatSettings;
begin
  l_FS := TFormatSettings.Create;
  l_FS.DecimalSeparator := '.';
  Result := SysUtils.FloatToStr(aValue, l_FS);
end;

Полный листинг нашего нового класса:
unit CalculatorOperationRandomSequenceTest;

interface

uses
  TestFrameWork,
  Calculator,
  Tests.Logger;

 type
  TCalcOperation = function (const A, B: string): string of object;

  TCalculatorOperationRandomSequenceTest = class(TTestCase)
   private
    procedure CheckOperation(aLogger: TLogger;
                             aX1, aX2: Double;
                             anOperation : TCalcOperation);
    procedure CheckOperationSeq(aLogger: TLogger;
                                anOperation : TCalcOperation);
   published
    procedure TestDiv;
    procedure TestMul;
    procedure TestAdd;
    procedure TestSub;
  end;//TCalculatorOperationRandomSequenceTest

implementation

  uses
    SysUtils;


{ TCalculatorOperationRandomSequenceTest }
procedure TCalculatorOperationRandomSequenceTest.CheckOperationSeq(
  aLogger: TLogger;
  anOperation: TCalcOperation);
var
  l_Index : Integer;
begin
  RandSeed := 40000;
  aLogger.OpenTest(Self);
  for l_Index := 0 to 10000 do
    CheckOperation(aLogger,
                   1000 * Random,
                   2000 * Random + 1, anOperation);
  CheckTrue(aLogger.CheckWithEtalon);
end;


procedure TCalculatorOperationRandomSequenceTest.CheckOperation(
  aLogger: TLogger;
  aX1, aX2: Double;
  anOperation : TCalcOperation);
begin
  aLogger.ToLog(aX1);
  aLogger.ToLog(aX2);
  aLogger.ToLog(anOperation(FloatToStr(aX1),FloatToStr(aX2)));
end;

procedure TCalculatorOperationRandomSequenceTest.TestDiv;
begin
  CheckOperationSeq(g_Logger, TCalculator.Divide);
end;

procedure TCalculatorOperationRandomSequenceTest.TestSub;
begin
  CheckOperationSeq(g_Logger, TCalculator.Sub);
end;

procedure TCalculatorOperationRandomSequenceTest.TestMul;
begin
  CheckOperationSeq(g_Logger, TCalculator.Mul);
end;

procedure TCalculatorOperationRandomSequenceTest.TestAdd;
begin
  CheckOperationSeq(g_Logger, TCalculator.Add);
end;

initialization
 TestFramework.RegisterTest(TCalculatorOperationRandomSequenceTest.Suite);
end.

В итоге в наше тестирование добавилось 40к тестов по 10к для каждой операции. А благодаря использованию "тестирования с использованием эталонов" результат всех тестов зафиксирован в системе контроля версий. При написании данного вида тестирования, мы не трогали Бизнес-логику, а на основе предыдущего, пусть и минимального, тестирования выявили что она верна. 

Таким образом мы перешли к от минимального тестирования приложения, к регрессионному тестированию. Которое будет полезно на протяжении всего "цикла жизни" нашего ПО.


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

  1. "RandSeed := 40000;"
    -- а вот этот параметр - тоже можно ввести в "состояние теста" и отразить в имени лога, таким образом можно получить несколько логов для разных RandSeed. Мы про это ещё напишем.

    ОтветитьУдалить
  2. По-моему - уже понятно, что "банальный тест" в "псевдослучайными данными" можно экстраполировать на множество алгоритмов, с разными входными параметрами. НЕ ОБЯЗАТЕЛЬНО вещественными или целочисленными.

    Достаточно ЗАФИКСИРОВАТЬ состояние БД и тогда можно "псевдослучайно" выбирать данные из БД и смотреть - НАСКОЛЬКО ДЕТЕРМИНИРОВАННО ведёт себя алгоритм расчёта. И нет ли ещё каких "неучтённых параметров".

    ОтветитьУдалить
  3. Добавлю ещё "в развитие темы", чтобы было понятно "к чему это всё".

    В предыдущих сериях мы худо-бедно:
    1. Протестировали приложение через GUI.
    2. Протестировали приложение через выделенный "слой" бизнес-логики.
    3. Протестировали приложение через выделенный "слой" бизнес логики и ввели понятие "эталонов" и протестировали (худо-бедно), что "эталоны работают".

    ТЕПЕРЬ мы задумались о "расширении тестового покрытия" и регрессионных тестах.

    Что мы собственно и сделали - тестируя операции калькулятора не для "единственной пары входных параметров", а для "последовательности пар входных параметров".

    ПОВТОРЮСЬ.

    Корректность "тестов с псевдослучайными данными" мы ВЫВОДИМ "по-индукции" опираясь лишь на ДОПУЩЕНИЯ того, что ВСЕ ПРЕДЫДУЩИЕ тесты более-менее КОРРЕКТНО работают.

    ОтветитьУдалить
  4. Очень интерестная статья. За RandSeed отдельное спасибо. Объявление типа в виде функции тоже занимательная вещь, жаль что раньше не знал, очень бы помогла.

    ОтветитьУдалить
    Ответы
    1. Ну статья конечно не про RandSeed и не про указатели на функции :-) но всё равно - ПОЖАЛУЙСТА. Читайте "следующие серии" :-) Они - будут...

      Удалить
  5. Тут надо бы дать ссылку на репозитарий.

    ОтветитьУдалить
    Ответы
    1. А! Она там есть - https://bitbucket.org/ingword/lulinproject/src/c223ad0b42b38cea5425a0ff51d01ccf19bcb6b8/DummyCalculator/Chapter6.2/?at=Release

      Удалить