2012-01-25 26 views
20

我正在使用DUnit测试Delphi库。我有时遇到了一些情况,我写了几个非常类似的测试来检查函数的多个输入。我可以在DUnit中写入“参数化”测试

有没有办法在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

你的意思是动态创建列表中所有输入值的测试用例吗?我的(小的)[OpenCTF](http://sourceforge.net/projects/openctf/)测试框架包含动态创建测试用例的代码。它基于DUnit。 – mjn

+0

您可以随时在测试类中编写一个通用参数化方法,并从一个或多个特定(已发布)的测试方法中调用该方法。 TestCase的Check(Not)Equals方法也可以帮助在这里帮助保持代码简洁,并且仍然为每个测试提供特定的失败消息。 –

+0

@Marjan只要第一个Check(Not)Equals失败,测试方法就会停止执行 - 动态创建测试用例解决了这个问题,所有其他值仍将被测试 – mjn

回答

3

如果DUnit允许编写这样的代码,那么AddTestForDoStuff的每次调用都会创建一个类似于您示例中的测试用例的测试用例吗?

Suite.AddTestForDoStuff.With(2).Expect(4); 
Suite.AddTestForDoStuff.With(3).Expect(9); 

我会尝试发布一个例子,如何这可以在今天晚些时候进行...


对于.NET已经有类似的东西:流利断言

http://www.codeproject.com/Articles/784791/Introduction-to-Unit-Testing-with-MS-tests-NUnit-a

+0

沿着这些线会很好。但它可能需要以具体测试作为参数,对吧?如'Suite.AddTest('DoStuff')。WithArgument(2).Expects(4)' –

+0

@MathiasFalkenberg:这或至少是添加消息的可能性。 –

+0

这个答案很酷。这是我第一次看到因为一厢情愿而获得的最高票数。是的,如果你能做到这一点,这不会太好!无论如何,如果您能够生成实际遵循的代码,将获得+1首付。 –

0

下面是一个使用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类进行编码,但我发现类而直观的使用。上述代码使我在编码检查和生成可理解的故障消息方面具有更大的灵活性。然而它的缺点是在第一次失败时停止测试。

11

我认为你正在寻找的东西是这样的:

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非常全面和灵活,每当我使用它时,我总会感到我错过了一个更好,更简单的方法来解决问题。

+2

我的感受是一样的......这就是我发布这个问题的原因。虽然你的代码肯定会产生所需的输出,但我希望我的测试更具可读性。 'CreateTest'方法给测试代码带来了一层复杂性,我真的宁愿避免...... –

19

您可以使用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

由于德尔福的属性只接受常量的属性只取参数作为一个字符串,其中值由分号分隔。但是没有任何东西阻止您创建自己的属性类,这些属性类会使用正确类型的多个参数来防止“魔术”字符串。无论如何,你仅限于可以是const的类型。

您也可以在方法的每个参数上指定Values属性,并使用任何可能的组合调用它(如在NUnit中)。

参考其他答案个人,我想编写单元测试时写尽可能少的代码。另外我想知道当我看到接口部分时没有通过实现部分挖掘测试做什么(我不打算说:“让我们做BDD”)。这就是为什么我更喜欢这种陈述式的方式。

+0

+1这看起来确实非常有用和有趣。感谢您将它引入我们的关注。 –

+0

+1确实!我一定会考虑一下。我认为这是迄今为止建议的最简单的方法! –

相关问题