问题
I am using DUnit to test a Delphi library. I sometimes run into cases, where i write several very similar tests to check multiple inputs to a function.
Is there a way to write (something resembling) a parameterized test in DUnit? For instance specifying an input and expected output to a suitable test procedure, then running the test suite and getting feedback on which of the multiple runs of the test failed?
(Edit: an example)
For example, suppose I had two tests like this:
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;
I might have even more of these tests that do exactly the same thing but with different inputs and expectations. I don't want to merge them into one test, because I would like them to be able to pass or fail independently.
回答1:
You can use DSharp to improve your DUnit tests. Especially the new unit DSharp.Testing.DUnit.pas (in Delphi 2010 and higher).
Just add it to your uses after TestFramework and you can add attributes to your test case. Then it could look like this:
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.
When you run it your test looks like this:
Since attributes in Delphi just accept constants the attributes just take the arguments as a string where the values are separated by a semicolon. But nothing prevents you from creating your own attribute classes that take multiple arguments of the correct type to prevent "magic" strings. Anyway you are limited to types that can be const.
You can also specify the Values attribute on each argument of the method and it gets called with any possible combination (as in NUnit).
Referring to the other answers personally I want to write as little code as possible when writing unit tests. Also I want to see what the tests do when I look at the interface part without digging through the implementation part (I am not going to say: "let's do BDD"). That is why I prefer the declarative way.
回答2:
I think you are looking for something like this:
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.
which looks like this in the GUI test runner:
I'd be very interested to know if I have gone about this in a sub-optimal way. DUnit is so incredibly general and flexible that whenever I use it I always end up feeling that I've missed a better, simpler way to solve the problem.
回答3:
Would it be sufficient if DUnit allowed to write code like this, where every call of AddTestForDoStuff would create a test case similar to those in your example?
Suite.AddTestForDoStuff.With(2).Expect(4);
Suite.AddTestForDoStuff.With(3).Expect(9);
I'll try to post an example how this can be done later today...
For .Net there is already something similar: Fluent Assertions
http://www.codeproject.com/Articles/784791/Introduction-to-Unit-Testing-with-MS-tests-NUnit-a
回答4:
Here is an example of using a general parameterized test method called from your TTestCase descendants actual (published) test methods (:
procedure TTester.CreatedWithoutDisplayFactorAndDisplayString;
begin
MySource := TMyClass.Create(cfSum);
SendAndReceive;
CheckDestinationAgainstSource;
end;
procedure TTester.CreatedWithDisplayFactorWithoutDisplayString;
begin
MySource := TMyClass.Create(cfSubtract, 10);
SendAndReceive;
CheckDestinationAgainstSource;
end;
Yes, there is some duplication, but the main duplication of code was taken out of these methods into the SendAndReceive and CheckDestinationAgainstSource methods in an ancestor class:
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;
The parameter in the CheckDestinationAgainstSource also allows for this type of use:
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;
This last test could also be coded using the TRepeatedTest class, but I find that class rather unintuitive to use. The above code gives me greater flexibility in coding checks and producing intelligible failure messages. It does however have the drawback of stopping the test on the first failure.
来源:https://stackoverflow.com/questions/8999945/can-i-write-parameterized-tests-in-dunit