Delphi 中DUnitX的使用指南

ioekq8ef  于 2023-03-01  发布在  其他
关注(0)|答案(1)|浏览(453)

多年来,我一直在编写紧密耦合的 Delphi 代码。我决定停止这样做,从现在开始只编写干净的可测试代码。因此,我一直在研究干净代码、依赖注入、重构、解耦、单元测试等(Nick Hodges、Ray Konopka、Uncle Bob、Alister Christie等)。
我被困在如何对我的一个类进行单元测试(使用DUnitX)上,目前为止我研究的所有例子都显示CUT的依赖接口被模拟,然后被注入到CUT的构造函数中,如下所示(来自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;

很好,我都理解。
不幸的是,我的类没有构造函数,它只有依赖项(是一个记录,而不是接口)在类过程中传递,如下所示:
这个单元包含了我的类实现的接口...

unit Unit15;

interface

type
  TMyRecord = record
    Flag: boolean
  end;

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

implementation

end.

这个单元包含我的类(实现了上面的接口)...

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.

你可以这样部署我的类...

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.

我创建了一个DUnitX项目,并开始像Nick的信用卡管理器示例那样进行。但这就是我认为我未能完全掌握单元测试中使用的技术的地方。我的测试项目有这个单元...

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.

我应该如何测试DoSomething在被调用时的行为是否正确?
可以按照我编写的那样测试MyClass吗?还是必须重新构造它?
有人能给我指一下正确的方向吗?

mutmk8jj

mutmk8jj1#

只能测试具有某些外部世界和测试框架可访问的副作用(状态更改)的方法和功能。如果无法观察要测试的方法的效果,则无法测试该方法。
在这种特殊情况下,测试能力意味着阅读控制台输出缓冲区,并检查是否写入了预期的输出。据我所知,有一个Windows API允许您这样做,但我不能说使用该API实现测试有多容易或困难。显然,写入控制台只是作为代码中的示例使用,但它显示了原理。
这个控制台实际上是一个硬编码的依赖项,你不应该在可测试代码中使用它,它是一个很难检查和验证正确性的依赖项,应该尽可能避免这样的依赖项,即使你有一些方法来验证副作用。
在您的情况下,这意味着将TConsoleWriter作为TMyClass的依赖项引入。然后,该依赖项将作为通过依赖项注入注入的参数传递。一旦您有了该依赖项,就可以用一些测试双精度浮点数(模拟)替换该依赖项,并且可以观察运行测试时发生的副作用,并验证是否得到了预期的结果。
如果代码的副作用与外部依赖项无关,而是封装在类本身中,则需要允许访问(只读就足够了)封装的数据,以便验证方法调用的结果。

相关问题