Глава 0.
Глава 1.
Глава 2.
Глава 3.
Глава 4.
Глава 5.
Глава 1.
Глава 2.
Глава 3.
Глава 4.
Глава 5.
В прошлой главе мы перешли от тестирования GUI формы, к тестированию бизнес-логики, выделив класс бизнес логики TCalculator.
В этой главе мы обсудим и внедрим “Тестирование с использованием эталонов”. Тестирование с использованием эталонов строится на базе тестов из прошлой главы. Однако выполняет более важную функцию, о чем будет рассказано далее. В двух словах -использование эталонов предполагает сохранение значений и результата теста в файл, который мы затем сравниваем с эталонным. Если файлы не совпадают то тест “провалился”. Тут возникает вопрос откуда мы возьмем эталонный файл? И здесь у нас 2 варианта: Либо мы его создадим руками, либо как поступил я - если эталона не существует, то мы создаем его автоматически на основе файла результата тестирования, так как допускаем что тесты у нас заведомо правильные.
Постараюсь более детально объяснить на примере написания эталонного теста к операции + или как она записана у нас в калькуляторе функция ADD.
Для начала напишем новый класс TCalculatorOperationViaEtalonTest, который очень похож на наш предыдущий класс TCalculatorOperationTest. Однако в этот раз мы проверяем не логику, а соответствие с эталонным файлом.
type TCalculatorOperationViaEtalonTest = class(TTestCase) published procedure TestDiv; procedure TestMul; procedure TestAdd; procedure TestSub; end;//TCalculatorOperationViaEtalonTest procedure TCalculatorOperationViaEtalonTest.TestAdd; var x1, x2 : string; begin x1:= cA; x2:= cB; CheckTrue(AddArgumentsToLog(g_Logger, x1, x2, TCalculator.Add(x2, x1), Self)); end;
Для сохранения результатов тестирования в файл и сравнения с эталоном, введём дополнительный класс TLogger который будем использовать через глобальную переменную g_Logger.
unit Tests.Logger; interface uses TestFrameWork; const cEtalonSuffix = '.etalon'; cTestSuffix = '.out'; cTestFolder = 'TestSet'; type TLogger = class strict private FTestFile : TextFile; FTestFilePath, FEtalonFilePath : string; private function TestOutputFolderPath: string; function Is2FilesEqual(const aFilePathTest, aFilePathEtalon: string): Boolean; function IsExistEtalonFile: Boolean; public class constructor Create; class destructor Destroy; procedure OpenTest(aTestCase: TTestCase); procedure ToLog(const aParametr: string); function CheckWithEtalon: Boolean; end;//TLogger var g_Logger : TLogger; implementation uses SysUtils, System.Classes, Winapi.Windows; { TLogger } function TLogger.CheckWithEtalon: Boolean; begin Assert(FTestFilePath<>''); Assert(FEtalonFilePath<>''); CloseFile(FTestFile); if IsExistEtalonFile then Result := Is2FilesEqual(FTestFilePath, FEtalonFilePath) else Result := CopyFile(PWideChar(FTestFilePath),PWideChar(FEtalonFilePath),True); end; class destructor TLogger.Destroy; begin FreeAndNil(g_Logger); end; class constructor TLogger.Create; begin g_Logger := TLogger.Create; end; function TLogger.Is2FilesEqual(const aFilePathTest, aFilePathEtalon: string): Boolean; var l_msFileTest, l_msFileEtalon: TMemoryStream; begin Result := False; l_msFileTest := TMemoryStream.Create; try l_msFileTest.LoadFromFile(aFilePathTest); l_msFileEtalon := TMemoryStream.Create; try l_msFileEtalon.LoadFromFile(aFilePathEtalon); if l_msFileTest.Size = l_msFileEtalon.Size then Result := CompareMem(l_msFileTest.Memory, l_msFileEtalon.memory, l_msFileTest.Size); finally FreeAndNil(l_msFileEtalon); end; finally FreeAndNil(l_msFileTest); end end; function TLogger.IsExistEtalonFile: Boolean; begin Result:= FileExists(FEtalonFilePath); end; procedure TLogger.OpenTest(aTestCase: TTestCase); var l_FileName : string; begin l_FileName := aTestCase.ClassName + aTestCase.GetName; FTestFilePath := TestOutputFolderPath + l_FileName + cTestSuffix; FEtalonFilePath := TestOutputFolderPath + l_FileName + cEtalonSuffix; if not DirectoryExists(TestOutputFolderPath) then ForceDirectories(TestOutputFolderPath); AssignFile(FTestFile, FTestFilePath); Rewrite(FTestFile); end; function TLogger.TestOutputFolderPath: string; begin Result := ExtractFilePath(ParamStr(0)) + cTestFolder + '\' end; procedure TLogger.ToLog(const aParametr: string); begin Writeln(FTestFile, aParametr + ' '); end; end.
Таким образом после выполнения тестирования в первый раз мы получим на выходе два файла:
- TCalculatorOperationViaEtalonTestTestAdd.out файл который формирует "наш тест"
- TCalculatorOperationViaEtalonTestTestAdd.etalon файл который стал(ещё раз подчеркну что мы предполагаем что наши тесты верны) эталоном. Этот эталон мы фиксируем в GIT.
Формат файлов .out и .etalon для операции "Плюс":
5 -- первый аргумент
10 -- второй аргумент
15 -- результат
Полный код нового класса TCalculatorOperationViaEtalonTest:
Ну а теперь самое интересное, зачем нам собственно нужны эталонные тесты. Полный код нового класса TCalculatorOperationViaEtalonTest:
unit CalculatorOperationViaEtalonTest; interface uses TestFrameWork, Calculator ; type TCalculatorOperationViaEtalonTest = class(TTestCase) published procedure TestDiv; procedure TestMul; procedure TestAdd; procedure TestSub; end;//TCalculatorOperationViaEtalonTest implementation uses SysUtils, Tests.Logger; const cA = '5'; cB = '10'; { TCalculatorOperationViaEtalonTest } function AddArgumentsToLog(aLogger: TLogger; aX1, aX2, aResult: string; aTestCase: TTestCase): Boolean; begin aLogger.OpenTest(aTestCase); aLogger.ToLog(aX1); aLogger.ToLog(aX2); aLogger.ToLog(aResult); Result := aLogger.CheckWithEtalon; end; procedure TCalculatorOperationViaEtalonTest.TestDiv; var x1, x2 : string; begin x1:= cA; x2:= cB; CheckTrue(AddArgumentsToLog(g_Logger, x1, x2, TCalculator.Divide(x2, x1), Self)); end; procedure TCalculatorOperationViaEtalonTest.TestSub; var x1, x2 : string; begin x1:= cA; x2:= cB; CheckTrue(AddArgumentsToLog(g_Logger, x1, x2, TCalculator.Sub(x2, x1), Self)); end; procedure TCalculatorOperationViaEtalonTest.TestMul; var x1, x2 : string; begin x1:= cA; x2:= cB; CheckTrue(AddArgumentsToLog(g_Logger, x1, x2, TCalculator.Mul(x2, x1), Self)); end; procedure TCalculatorOperationViaEtalonTest.TestAdd; var x1, x2 : string; begin x1:= cA; x2:= cB; CheckTrue(AddArgumentsToLog(g_Logger, x1, x2, TCalculator.Add(x2, x1), Self)); end; initialization TestFramework.RegisterTest(TCalculatorOperationViaEtalonTest.Suite); end.
1. Так как эталонный тест зафиксирован в GIT то мы всегда сможем узнать не сломалось ли у нас что то по ходу новых изменений.
2. "Наиболее" главное. В больших командах разработка поделена на разработчиков и тестировщиков. И если меняется логика программы, например функция Плюс стала не просто складывать 2 числа, а складывать 2 числа и добавлять к ним 8, то мы легко увидим этот факт.
class function TCalculator.Add(const A, B: string): string; var x1, x2, x3 : single; begin x1 := StrToFloat(A); x2 := StrToFloat(B); x3 := x1 + x2 + 8; Result := FloatToStr(x3); end;
Теперь если мы запустим наше тестирование то выглядеть оно будет так:
То есть все наши тесты "Плюса" упали. И соответственно разработчику надо переделать 3 теста. Однако давайте попробуем просто подправить эталон.
Было:
5
10
15
Стало:
5
10
23
10
23
И теперь:
Как видим тесты с эталонами выполнились успешно. То есть при изменении логики программы изменение тестов могут выполнять и тестировщики, БЕЗ НЕОБХОДИМОСТИ что-то изменять в исходниках. Что нам дает огромную гибкость для применения.
P.S.
При написании статьи пытался обнаружить раскрытие темы в рунете. Тема в рунете неразвита. Однако похожая схема тестирования описана у Кернигана и Пайка. В главе про тестирование. Правда там они предлагают замерять время выполнения теста, и производительность при прохождении. Однако к этому мы дойдем в наших будущих статьях.
Source code.
Там есть проблемы с региональными настройками и "запятыми", это кстати повод для отдельного разговора.
ОтветитьУдалитьfunction FloatToStrP(const AValue: Extended; ADecSep: Char): string;
Удалитьvar
FS: TFormatSettings;
begin
GetLocaleFormatSettings(LOCALE_SYSTEM_DEFAULT, FS);
FS.DecimalSeparator := ADecSep;
Result := FloatToStr(AValue, FS);
end;
//с запятой
str := FloatToStrP(123.456, ',');
//с точкой
str := FloatToStrP(123.456, '.');
Ну "закостылять" то мы закостыляли. Но вообще говоря это повод для отдельного разговора про зависимость тестов от клиентского окружения.
УдалитьПравда можно посмотреть на проблему "запятой" и в ином разрезе - как на ОШИБКУ, которая пришла от "пользователя", только "пользователем" в этом случае выступает РАЗРАБОТЧИК.
ОтветитьУдалитьВы разносите тест в несколько мест. Появляется с одной стороны гибкость - для тестировщика возможность подкорректировать быстро результаты. Но для программиста это может вносить сложность - файл с результатами нужно найти на диске. В C# в фреймворке NUnit есть такое понятие как TestCase - один метод теста можно прогнать множеством параметров или даже подсунуть метод который читает параметры из файлов и т.д. Второй вариант гибче но не нагляднее. Лучше когда код теста полностью перед глазами прост и легок.
ОтветитьУдалитьВы внедрили этот способ тестирования полгода назад. Какие результаты эксперимента? Какие плюсы и минусы? Как изменяется работа тестов когда меняется сигнатура методов?
Мы внедрили не "пол-года" назад. А лет пять назад. А то и больше. И это НЕ "эксперимент", это ПРОМЫШЛЕННОЕ использование.
Удалить"Вы разносите тест в несколько мест"
Удалить-- непонятно, что вы имеете в виду.