Глава 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 - один метод теста можно прогнать множеством параметров или даже подсунуть метод который читает параметры из файлов и т.д. Второй вариант гибче но не нагляднее. Лучше когда код теста полностью перед глазами прост и легок.
ОтветитьУдалитьВы внедрили этот способ тестирования полгода назад. Какие результаты эксперимента? Какие плюсы и минусы? Как изменяется работа тестов когда меняется сигнатура методов?
Мы внедрили не "пол-года" назад. А лет пять назад. А то и больше. И это НЕ "эксперимент", это ПРОМЫШЛЕННОЕ использование.
Удалить"Вы разносите тест в несколько мест"
Удалить-- непонятно, что вы имеете в виду.