Могу ли я писать "параметризованные" тесты в DUnit
Я использую DUnit для тестирования библиотеки Delphi. Я иногда сталкиваюсь с случаями, когда я пишу несколько очень похожих тестов, чтобы проверять несколько входов на функцию.
Есть ли способ написать (что-то похожее) параметризованный тест в DUnit? Например, указав входной и ожидаемый выходные данные на подходящую тестовую процедуру, затем запустив тестовый набор и получив обратную связь о том, какой из нескольких прогонов теста не удался?
(Изменить: пример)
Например, предположим, что у меня было два теста:
procedure TestMyCode_WithInput2_Returns4();
var
Sut: TMyClass;
Result: Integer;
begin
// Arrange:
Sut := TMyClass.Create;
// Act:
Result := sut.DoStuff(2);
// Assert
CheckEquals(4, Result);
end;
procedure TestMyCode_WithInput3_Returns9();
var
Sut: TMyClass;
Result: Integer;
begin
// Arrange:
Sut := TMyClass.Create;
// Act:
Result := sut.DoStuff(3);
// Assert
CheckEquals(9, Result);
end;
У меня может быть даже больше этих тестов, которые будут делать то же самое, но с разными затратами и ожиданиями. Я не хочу объединять их в один тест, потому что я хотел бы, чтобы они могли самостоятельно проходить или отказываться.
Ответы
Ответ 1
Вы можете использовать DSharp для улучшения ваших тестов DUnit. Особенно новый блок DSharp.Testing.DUnit.pas (в Delphi 2010 и выше).
Просто добавьте его в свои приложения после TestFramework, и вы можете добавить атрибуты в свой тестовый пример. Тогда это может выглядеть так:
unit MyClassTests;
interface
uses
MyClass,
TestFramework,
DSharp.Testing.DUnit;
type
TMyClassTest = class(TTestCase)
private
FSut: TMyClass;
protected
procedure SetUp; override;
procedure TearDown; override;
published
[TestCase('2;4')]
[TestCase('3;9')]
procedure TestDoStuff(Input, Output: Integer);
end;
implementation
procedure TMyClassTest.SetUp;
begin
inherited;
FSut := TMyClass.Create;
end;
procedure TMyClassTest.TearDown;
begin
inherited;
FSut.Free;
end;
procedure TMyClassTest.TestDoStuff(Input, Output: Integer);
begin
CheckEquals(Output, FSut.DoStuff(Input));
end;
initialization
RegisterTest(TMyClassTest.Suite);
end.
Когда вы запускаете его, ваш тест выглядит следующим образом:
![enter image description here]()
Поскольку атрибуты в Delphi просто принимают константы, атрибуты просто принимают аргументы как строку, где значения разделяются точкой с запятой. Но ничто не мешает вам создавать свои собственные классы атрибутов, которые принимают несколько аргументов правильного типа, чтобы предотвратить "магические" строки. В любом случае вы ограничены типами, которые могут быть const.
Вы также можете указать атрибут Values для каждого аргумента метода и получать его с любой возможной комбинацией (как в NUnit).
Обращаясь к другим ответам лично, я хочу писать как можно меньше кода при написании модульных тестов. Также я хочу посмотреть, что делают тесты, когда я смотрю на часть интерфейса, не копая часть реализации (я не собираюсь говорить: "let do BDD" ). Вот почему я предпочитаю декларативный способ.
Ответ 2
Я думаю, вы ищете что-то вроде этого:
unit TestCases;
interface
uses
SysUtils, TestFramework, TestExtensions;
implementation
type
TArithmeticTest = class(TTestCase)
private
FOp1, FOp2, FSum: Integer;
constructor Create(const MethodName: string; Op1, Op2, Sum: Integer);
public
class function CreateTest(Op1, Op2, Sum: Integer): ITestSuite;
published
procedure TestAddition;
procedure TestSubtraction;
end;
{ TArithmeticTest }
class function TArithmeticTest.CreateTest(Op1, Op2, Sum: Integer): ITestSuite;
var
i: Integer;
Test: TArithmeticTest;
MethodEnumerator: TMethodEnumerator;
MethodName: string;
begin
Result := TTestSuite.Create(Format('%d + %d = %d', [Op1, Op2, Sum]));
MethodEnumerator := TMethodEnumerator.Create(Self);
Try
for i := 0 to MethodEnumerator.MethodCount-1 do begin
MethodName := MethodEnumerator.NameOfMethod[i];
Test := TArithmeticTest.Create(MethodName, Op1, Op2, Sum);
Result.addTest(Test as ITest);
end;
Finally
MethodEnumerator.Free;
End;
end;
constructor TArithmeticTest.Create(const MethodName: string; Op1, Op2, Sum: Integer);
begin
inherited Create(MethodName);
FOp1 := Op1;
FOp2 := Op2;
FSum := Sum;
end;
procedure TArithmeticTest.TestAddition;
begin
CheckEquals(FOp1+FOp2, FSum);
CheckEquals(FOp2+FOp1, FSum);
end;
procedure TArithmeticTest.TestSubtraction;
begin
CheckEquals(FSum-FOp1, FOp2);
CheckEquals(FSum-FOp2, FOp1);
end;
function UnitTests: ITestSuite;
begin
Result := TTestSuite.Create('Addition/subtraction tests');
Result.AddTest(TArithmeticTest.CreateTest(1, 2, 3));
Result.AddTest(TArithmeticTest.CreateTest(6, 9, 15));
Result.AddTest(TArithmeticTest.CreateTest(-3, 12, 9));
Result.AddTest(TArithmeticTest.CreateTest(4, -9, -5));
end;
initialization
RegisterTest('My Test cases', UnitTests);
end.
который выглядит так в драйвере GUI:
![enter image description here]()
Мне было бы очень интересно узнать, не обходился ли я этим в субоптимальном виде. DUnit настолько невероятно общий и гибкий, что всякий раз, когда я его использую, я всегда чувствую, что пропустил более простой и простой способ решить проблему.
Ответ 3
Было бы достаточно, если бы DUnit разрешал писать такой код, где каждый вызов AddTestForDoStuff создавал бы тестовый пример, аналогичный тому, который приведен в вашем примере?
Suite.AddTestForDoStuff.With(2).Expect(4);
Suite.AddTestForDoStuff.With(3).Expect(9);
Я попытаюсь опубликовать пример, как это можно сделать позже сегодня...
Для .Net уже есть что-то подобное: Fluent Assertions
http://www.codeproject.com/Articles/784791/Introduction-to-Unit-Testing-with-MS-tests-NUnit-a
Ответ 4
Ниже приведен пример использования общего параметризованного метода тестирования, который вызывается из ваших фактических (опубликованных) тестовых методов ваших потомков TTestCase (:
procedure TTester.CreatedWithoutDisplayFactorAndDisplayString;
begin
MySource := TMyClass.Create(cfSum);
SendAndReceive;
CheckDestinationAgainstSource;
end;
procedure TTester.CreatedWithDisplayFactorWithoutDisplayString;
begin
MySource := TMyClass.Create(cfSubtract, 10);
SendAndReceive;
CheckDestinationAgainstSource;
end;
Да, существует некоторое дублирование, но основное дублирование кода было выведено из этих методов в методы SendAndReceive и CheckDestinationAgainstSource в классе предков:
procedure TCustomTester.SendAndReceive;
begin
MySourceBroker.CalculationObject := MySource;
MySourceBroker.SendToProtocol(MyProtocol);
Check(MyStream.Size > 0, 'Stream does not contain xml data');
MyStream.Position := 0;
MyDestinationBroker.CalculationObject := MyDestination;
MyDestinationBroker.ReceiveFromProtocol(MyProtocol);
end;
procedure TCustomTester.CheckDestinationAgainstSource(const aCodedFunction: string = '');
var
ok: Boolean;
msg: string;
begin
if aCodedFunction = '' then
msg := 'Calculation does not match: '
else
msg := 'Calculation does not match. Testing CodedFunction ' + aCodedFunction + ': ';
ok := MyDestination.IsEqual(MySource, MyErrors);
Check(Ok, msg + MyErrors.Text);
end;
Параметр в CheckDestinationAgainstSource также позволяет использовать этот тип использования:
procedure TAllTester.AllFunctions;
var
CF: TCodedFunction;
begin
for CF := Low(TCodedFunction) to High(TCodedFunction) do
begin
TearDown;
SetUp;
MySource := TMyClass.Create(CF);
SendAndReceive;
CheckDestinationAgainstSource(ConfiguredFunctionToString(CF));
end;
end;
Этот последний тест также может быть закодирован с использованием класса TRepeatedTest, но я считаю, что этот класс довольно неинтуитивный для использования. Вышеприведенный код дает мне большую гибкость в проверках кодирования и создании понятных сообщений об отказах. Тем не менее, у него есть недостаток остановки теста при первом сбое.