DUnitX在Delphi中的使用指南

huangapple go评论79阅读模式
英文:

Guidance on use of DUnitX in Delphi

问题

I understand that you want a translation of your provided text, excluding the code part. Here's the translated text:

"For too many years I have been writing tightly-coupled spaghetti Delphi code.
I have decided to stop doing this and write only clean testable code from now on.
So, I have been studying clean code, dependency injection, refactoring, decoupling, unit testing, etc, etc (Nick Hodges, Ray Konopka, Uncle Bob, Alister Christie to name a few).

I'm stuck on how to go about unit testing one of my classes (using DUnitX). All the examples I have researched so far show the CUT's dependency interface being mocked and then being injected in the CUT's constructor, like this (from Nick Hodges):

That's fine and I understand all of it.

Unfortunately, my class does not have a constructor. It's only dependency (which is a record, not an interface) is passed in a class procedure, as follows:

This unit contains the interface implemented by my class...

This unit contains my class (that implements the above interface)...

And here is how you might deploy my class...

I created a DUnitX project and started to proceed like Nick's credit card manager example. But this is where I think I have failed to fully grasp the techniques used in unit testing. My Test project has this unit...

How should I test that DoSomething behaves correctly when called?

Is it possible to test MyClass as I have written it or must I restructure it?

Can someone point me in the right direction?"

英文:

For too many years I have been writing tightly-coupled spaghetti Delphi code.
I have decided to stop doing this and write only clean testable code from now on.
So, I have been studying clean code, dependency injection, refactoring, decoupling, unit testing, etc, etc (Nick Hodges, Ray Konopka, Uncle Bob, Alister Christie to name a few).

I'm stuck on how to go about unit testing one of my classes (using DUnitX). All the examples I have researched so far show the CUT's dependency interface being mocked and then being injected in the CUT's constructor, like this (from Nick Hodges):

procedure TestTCCValidator.TestCardChargeReturnsProperAmountWhenCardIsGood;
var
  CCManager: TCreditCardManager;
  CCValidator: TMock<ICreditCardValidator>;
  GoodCard: String;
  Input: Double;
  Expected, Actual: Double;
begin
  //Arrange
  GoodCard := '123456';
  Input := 49.95;
  Expected := Input;
  CCValidator := TMock<ICreditCardValidator>.Create;
  CCValidator.Setup.WillReturn(True).When.IsCreditCardValid(GoodCard);
  CCManager := TCreditCardManager.Create(CCValidator);
  try
  //Act
  Actual := CCManager.ProcessCreditCard(GoodCard, Input)
  finally
  CCManager.Free;
  end;
  // Assert
Assert.AreEqual(Expected, Actual);
end;

That's fine and I understand all of it.

Unfortunately, my class does not have a constructor. It's only dependency (which is a record, not an interface) is passed in a class procedure, as follows:

This unit contains the interface implemented by my class...

unit Unit15;

interface

type
  TMyRecord = record
    Flag: boolean
  end;

  IMyInterface = interface(IInvokable)
    ['{17783F54-0F63-413B-8198-88705CBA318F}']
    procedure DoSomething;
  end;


implementation

end.

This unit contains my class (that implements the above interface)...

unit Unit14;

interface

uses
  Unit15;

type
  TMyClass = class(TInterfacedObject, IMyInterface)
    FRec: TMyRecord;
    procedure DoSomething;
    procedure DoThis;
    procedure DoThat;
    class procedure Execute(ARec: TMyRecord);
  end;

implementation

{ MyClass }

procedure TMyClass.DoSomething;
begin
  case FRec.Flag of
    true: DoThis;
    false: DoThat;
  end;
end;

procedure TMyClass.DoThat;
begin
  writeln('did that');
end;

procedure TMyClass.DoThis;
begin
  writeln('did this');
end;

class procedure TMyClass.Execute(ARec: TMyRecord);
begin
  var mc: IMyInterface := TMyClass.Create;
  TMyClass(mc).FRec := ARec;
  mc.DoSomething;
end;

end.

And here is how you might deploy my class...

program Project12;

{$APPTYPE CONSOLE}

{$R *.res}

uses
  System.SysUtils,
  Unit14 in 'Unit14.pas',
  Unit15 in 'Unit15.pas';

var
  Rec: TMyRecord;

begin
  try
    Rec.Flag := true;
    TMyClass.Execute(Rec);
    ReadLn;
  except
    on E: Exception do
      Writeln(E.ClassName, ': ', E.Message);
  end;
end.

I created a DUnitX project and started to proceed like Nick's credit card manager example. But this is where I think I have failed to fully grasp the techniques used in unit testing. My Test project has this unit...

unit Unit16;

interface

uses
  Unit14,
  Unit15,
  DUnitX.TestFramework;

type
  [TestFixture]
  TMyTestObject = class
  private
    FRec: TMyRecord;
    CUT: TMyClass;     // do I need this?
  public
    [Setup]
    procedure Setup;
    [TearDown]
    procedure TearDown;
    [Test]
//    [TestCase('Text Execute with true flag', true)]   // causes compiler error
//    [TestCase('Text Execute with false flag', false)]  // causes compiler error
    procedure TestExecute(const AFlag: boolean);
  end;

  // how do I mock IMyInterface (which is created in TMyClass.Execute)?

implementation

procedure TMyTestObject.Setup;
begin
end;

procedure TMyTestObject.TearDown;
begin
end;

procedure TMyTestObject.TestExecute(const AFlag: boolean);
begin
  FRec.Flag := AFlag;
  TMyClass.Execute(FRec);
end;

initialization
  TDUnitX.RegisterTestFixture(TMyTestObject);

end.

How should I test that DoSomething behaves correctly when called?

Is it possible to test MyClass as I have written it or must I restructure it?

Can someone point me in the right direction?

答案1

得分: 3

你只能测试具有某些外部世界和测试框架可访问的副作用(状态更改)的方法和功能。如果您无法观察要测试的方法的效果,那么就无法测试它。

在这种特定情况下,能够测试的能力意味着读取控制台输出缓冲区并检查是否获得了预期的输出。据我所知,有一个Windows API允许您这样做,但我无法说使用该API实施测试会有多容易或多困难。显然,在您的代码中,将写入控制台仅用作示例,但它展示了原则。

那个控制台实际上是您不应该在可测试代码中拥有的硬编码依赖项。这是一种使检查和验证正确性变得困难的依赖性,应尽量避免使用,即使您有某种手段来验证副作用。

在您的情况下,这意味着将TConsoleWriter引入TMyClass作为依赖项。然后,该依赖项将作为参数传递或通过依赖注入注入。一旦您拥有了它,您就可以用某种测试替身(模拟)替换该依赖项,然后您可以观察在运行测试时发生的副作用,并验证是否获得了预期的结果。

如果您的代码的副作用与外部依赖无关,而是封装在类本身内部,那么您需要允许访问(只读即可)允许您验证方法调用结果的封装数据。

英文:

You can only test methods and functionality that has some side-effects (state changes) accessible to the outside world and test framework. If you cannot observe the effects of the method you want to test, then you cannot test it.

In this particular case ability to test would mean reading the console output buffer and checking whether you got the expected output written. As far as I know there is a Windows API that allows you to do this, but I cannot say how easy or hard would be to implement test using that API. Obviously, writing to console is used just as example in your code, but it shows the principle.

That console is actually a hard coded dependency you shouldn't have in testable code. It is a dependency that makes it hard to check and verify for correctness and such dependencies should be avoided as much as possible, even if you have some means to verify the side-effects.

In your case that would mean introducing TConsoleWriter as dependency for TMyClass. That dependency would be then passed as a parameter of injected through dependency injection. Once you have that, then you can replace that dependency with some test double (mock) and you can observe the side-effects that happen when you run your test and verify whether you got the expected results.

If the side-effects of your code are not related to outside dependency, but are encapsulated within the class itself, then you need to allow access (read-only will suffice) to encapsulated data that allows you to verify the results of a method call.

huangapple
  • 本文由 发表于 2023年2月27日 03:58:12
  • 转载请务必保留本文链接:https://go.coder-hub.com/75574660.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定