我可以写“参数化”吗? DUnit 中的测试

发布于 2024-12-29 03:26:41 字数 734 浏览 0 评论 0 原文

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

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

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.

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(5

叶落知秋 2025-01-05 03:26:41

您可以使用 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")。这就是为什么我更喜欢声明式方式。

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:

enter image description here

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.

旧人九事 2025-01-05 03:26:41

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

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 测试运行程序中看起来像这样:

在此处输入图像描述

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

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:

enter image description here

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.

腹黑女流氓 2025-01-05 03:26:41

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

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

溺ぐ爱和你が 2025-01-05 03:26:41

下面是一个使用从 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 类进行编码,但我发现该类相反使用起来不直观。上面的代码在编码检查和生成可理解的失败消息方面提供了更大的灵活性,但它确实有在第一次失败时停止测试的缺点。

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.

昔梦 2025-01-05 03:26:41

至少在 Delphi XE8 中,开箱即用的 DUnitX 具有类似的属性:(

[TestCase('Test1', '1,one')]
[TestCase('Test2', '2,two')]
procedure TestIntToStr(AValInt: Integer; const AValString: string);

另请参阅 https://docwiki.embarcadero.com/RADStudio/Seattle/en/DUnitX_Overviewhttps://web.archive.org/web/20150504232249/http://docwiki.embarcadero.com/RADStudio/XE8/en/DUnitX_Overview

也可用于 Delphi 从 2010 版本开始 https://github.com/VSoftTechnologies/DUnitX

At least in Delphi XE8 just out the box coming a DUnitX with a similar attribute:

[TestCase('Test1', '1,one')]
[TestCase('Test2', '2,two')]
procedure TestIntToStr(AValInt: Integer; const AValString: string);

(See also https://docwiki.embarcadero.com/RADStudio/Seattle/en/DUnitX_Overview and https://web.archive.org/web/20150504232249/http://docwiki.embarcadero.com/RADStudio/XE8/en/DUnitX_Overview)

Also it available for Delphi from version 2010 at https://github.com/VSoftTechnologies/DUnitX

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文