Имея в распоряжении 4 теста для 4 операций мы точно не проверяем "большой разброс" во входных данных. Для того чтобы расширить наше тестовое покрытие мы переходим к нашей новой главе - "Тестированию с использованием псевдослучайных данных". Как следует из названия, нам необходим какой-то случайный набор данных для тестирования. Результаты тестирования при этом будут записываться в выходной файл, так же как мы делали это в прошлой главе.
Введем "тип функции" 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;
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к для каждой операции. А благодаря использованию "тестирования с использованием эталонов" результат всех тестов зафиксирован в системе контроля версий. При написании данного вида тестирования, мы не трогали Бизнес-логику, а на основе предыдущего, пусть и минимального, тестирования выявили что она верна.
Таким образом мы перешли к от минимального тестирования приложения, к регрессионному тестированию. Которое будет полезно на протяжении всего "цикла жизни" нашего ПО.
"RandSeed := 40000;"
ОтветитьУдалить-- а вот этот параметр - тоже можно ввести в "состояние теста" и отразить в имени лога, таким образом можно получить несколько логов для разных RandSeed. Мы про это ещё напишем.
По-моему - уже понятно, что "банальный тест" в "псевдослучайными данными" можно экстраполировать на множество алгоритмов, с разными входными параметрами. НЕ ОБЯЗАТЕЛЬНО вещественными или целочисленными.
ОтветитьУдалитьДостаточно ЗАФИКСИРОВАТЬ состояние БД и тогда можно "псевдослучайно" выбирать данные из БД и смотреть - НАСКОЛЬКО ДЕТЕРМИНИРОВАННО ведёт себя алгоритм расчёта. И нет ли ещё каких "неучтённых параметров".
Добавлю ещё "в развитие темы", чтобы было понятно "к чему это всё".
ОтветитьУдалитьВ предыдущих сериях мы худо-бедно:
1. Протестировали приложение через GUI.
2. Протестировали приложение через выделенный "слой" бизнес-логики.
3. Протестировали приложение через выделенный "слой" бизнес логики и ввели понятие "эталонов" и протестировали (худо-бедно), что "эталоны работают".
ТЕПЕРЬ мы задумались о "расширении тестового покрытия" и регрессионных тестах.
Что мы собственно и сделали - тестируя операции калькулятора не для "единственной пары входных параметров", а для "последовательности пар входных параметров".
ПОВТОРЮСЬ.
Корректность "тестов с псевдослучайными данными" мы ВЫВОДИМ "по-индукции" опираясь лишь на ДОПУЩЕНИЯ того, что ВСЕ ПРЕДЫДУЩИЕ тесты более-менее КОРРЕКТНО работают.
Очень интерестная статья. За RandSeed отдельное спасибо. Объявление типа в виде функции тоже занимательная вещь, жаль что раньше не знал, очень бы помогла.
ОтветитьУдалитьНу статья конечно не про RandSeed и не про указатели на функции :-) но всё равно - ПОЖАЛУЙСТА. Читайте "следующие серии" :-) Они - будут...
УдалитьТут надо бы дать ссылку на репозитарий.
ОтветитьУдалитьА! Она там есть - https://bitbucket.org/ingword/lulinproject/src/c223ad0b42b38cea5425a0ff51d01ccf19bcb6b8/DummyCalculator/Chapter6.2/?at=Release
Удалить