我可以在 DUnit 中编写“参数化”测试吗

Posted

技术标签:

【中文标题】我可以在 DUnit 中编写“参数化”测试吗【英文标题】:Can I write 'parameterized' tests in DUnit 【发布时间】:2012-02-18 11:22:07 【问题描述】:

我正在使用 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;

我可能有更多这样的测试,它们做的事情完全相同,但输入和期望不同。我不想将它们合并到一个测试中,因为我希望它们能够独立通过或失败。

【问题讨论】:

您的意思是为列表中的所有输入值动态创建测试用例吗?我的(小)OpenCTF 测试框架包含用于动态创建测试用例的代码。它基于 DUnit。 您总是可以在测试类中编写一个通用的参数化方法,并从一个或多个特定(已发布)的测试方法中调用它。 TestCase 的 Check(Not)Equals 方法在这里也可以提供帮助,以帮助保持代码简洁,并且仍然为每个测试提供特定的失败消息。 @Marjan 测试方法将在第一个 Check(Not)Equals 失败后立即停止执行 - 动态创建测试用例解决了这个问题,所有其他值仍将被测试 @mjn : OpenCTF 似乎是用于以黑盒方式测试组件和表单......这似乎不适用于这里...... @MarjanVenema :我猜这不是一个坏方法。我会试试那个... 【参考方案1】:

如果 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

【讨论】:

类似的东西会很好。但它可能也需要将特定测试作为论据,对吧?如Suite.AddTest('DoStuff').WithArgument(2).Expects(4) @MathiasFalkenberg:或者至少可以添加一条消息。 这个答案很酷。这是我第一次看到因一厢情愿而获得赞成票。是啊,如果你能做到这一点,那不是很好吗!!无论如何,如果您可以编写实际执行的代码,您将获得 +1 首付。 提示:AddTestForDoStuff 创建一个 TTestDoStuff 实例,With 和 Expect 像属性设置器一样工作。它是构建器模式,应用于 DUnit。【参考方案2】:

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

当你运行它时,你的测试看起来像这样:

因为 Delphi 中的属性只接受常量,所以属性只接受参数作为字符串,其中值用分号分隔。但是没有什么能阻止您创建自己的属性类,这些属性类采用正确类型的多个参数来防止“魔术”字符串。无论如何,您仅限于可以是 const 的类型。

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

个人参考其他答案我想在编写单元测试时编写尽可能少的代码。我还想看看当我查看接口部分而不深入研究实现部分时测试做了什么(我不会说:“让我们做BDD”)。这就是为什么我更喜欢声明式的方式。

【讨论】:

+1 这看起来确实非常有用和有趣。感谢您提请我们注意。 +1 确实!我一定会调查的。我认为这是迄今为止建议的最轻松的方法!【参考方案3】:

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

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 测试运行程序中如下所示:

我很想知道我是否以次优的方式解决了这个问题。 DUnit 非常通用和灵活,以至于每当我使用它时,我总觉得我错过了一种更好、更简单的方法来解决问题。

【讨论】:

我也有同样的感觉......这就是我发布这个问题的原因。虽然您的代码肯定会产生所需的输出,但我希望我的测试更具可读性。 'CreateTest' 方法为测试代码引入了一层复杂性,我真的很想避免......【参考方案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 类进行编码,但我发现该类使用起来相当不直观。上面的代码在编码检查和生成可理解的故障消息方面给了我更大的灵活性。但是,它确实有在第一次失败时停止测试的缺点。

【讨论】:

以上是关于我可以在 DUnit 中编写“参数化”测试吗的主要内容,如果未能解决你的问题,请参考以下文章

29. 使用参数化编写自动化测试用例

如何创建参数化的Jenkins作业?

性能工具之 nGrinder 参数化脚本编写

是否可以使用 Spring ApplicationContext 中的 bean 参数化 JUnit Jupiter 测试?

6_Selenium参数化

GoogleTest(参数化)-4