1

我正在模拟一个对象支持接口 IClient 以用于单元测试。

接口本身在另一个单元 ClientIF 中定义。

该接口引用了在另一个单元 ldetail 中定义的 TDetail。

如果我模拟 TDetail,我会在任何使用 TDetail 的函数上得到一个未识别的错误。

[DCC Error] MockClient.pas(16): E2003 Undeclared identifier: 'GetDetailValue'
[DCC Error] MockClient.pas(16): E2003 Undeclared identifier: 'GetDetail'
[DCC Error] MockClient.pas(16): E2003 Undeclared identifier: 'GetActiveDetail'
[DCC Fatal Error] EmailPdfPropertiesGeneratorTests.pas(25): F2063 Could not compile used unit 'MockClient.pas'

我需要能够模拟接口和支持的细节对象来测试我正在处理的单元/接口。否则,这一切都与后端数据有关,这将是一场测试噩梦。

相关 MockClient.pas 单元代码

uses ClientIF, MockDetail;

TMockClient = class(TInterfacedObject, IClient)
  FDetail: TDetail;
  function GetDetailValue: TDetail;
  function GetDetail: TDetail;
  function GetActiveDetail: TDetail;
  function GetDetailName: String;
end;

相关ClientIF.pas接口代码

uses Classes, TaxConst, OSIConst, ldetail, clNotesIF, MissingDataDefIF,
      ClientDataChangeEventIF;

    type
      IClient = interface
        ['{CFED9A10-1601-11D4-ACF6-005004889419}']
        function GetDetailValue: TDetail;
        function GetDetail: TDetail;
        function GetActiveDetail: TDetail;
        function GetDetailName: String;
      end;

相关MockDetail.pas代码

uses Classes;

type

  TCodeValuesRec = class
    private
      fAmount: double;
      fDesc: String;
      fStateID: integer;
      fCityID: String;
      fSuffixCount: integer;
    public
      property Amount: double read fAmount write fAmount;
      property Desc: String read fDesc write fDesc;
      property StateID: integer read fStateID write fStateID;
      property CityID: String read fCityID write fCityID;
      property SuffixCount: integer read fSuffixCount write fSuffixCount;
  end;

  TDetail = class
  private
    FSeries: Integer;
    FProp: Integer;
    FPropCount: Integer;
    FCodeValuesRec: TCodeValuesRec;
  public
    property Series: Integer read FSeries write FSeries;
    property Prop: Integer read FProp write FProp;
    property PropCount: Integer read FPropCount write FPropCount;
    function GetCodeValuesRecord(WhichRecord: Integer):TCodeValuesRec;
    constructor Create;
    destructor Destroy; override;
  end;

如果我在 MockClient uses 子句中将 MockDetail 替换为 ldetail ,它会编译,但是,当然,细节是我必须模拟的事情之一,因为它是来自被测代码的调用之一。

我们正在尝试对遗留代码进行测试,这是一个过程。有这个问题的代码实际上是新代码,但第一次需要旧对象进行测试。

这个问题的目标是测试新代码,因此创建一个旧接口的模拟(其中包含一个旧类),以便 MyClient.GetDetail 将返回填充了我可以在对象中使用的信息的模拟 TDetail正在测试中。如果不重构就无法伪造旧代码,那么该过程将不得不等待。

如果我可以让假的 Client 和 Detail 完成他们的假工作并编译到测试框架 (DUnit) 中,那么我的测试就可以针对真实的(新)代码运行,这就足够了,并且现在可以进行所有这些工作。

我们目前正在使用 Delphi 2010(今年已升级),并且最终将使用 XE,但我还不能使用 Mock 框架,因为它似乎只能与 XE2 一起使用。

4

1 回答 1

2

您必须针对接口进行编码和/或使 TDetail 成为“真实”和模拟 Detail 类的(抽象)祖先。否则,您将继续造成涟漪效应。

您需要找到一种方法来为创建 TDetail 实例的任何内容提供一种“注入”要使用的实际类的方法。想想依赖注入,但请记住,依赖注入是一个概念,而不是一个框架。

为了让你的代码减少对特定类的依赖,同时保留使用类特定构造函数的能力(而不是大多数成熟的依赖注入实现所需的标准无参数构造函数),元类可以来拯救。它们允许您使用自己特定的构造函数来实例化一个类。

type
  TDetail = class(TInterfacedObject)
  end;

  TDetailClass = class of TDetail; // Meta class declaration

  function SomeFunction(const aDetailClass: TDetailClass): TDetail;
  begin
    Result := aDetailClass.Create({whatever parameters the TDetail constructor needs});
  end;

使用元类和 TInterfacedObject 后代的一个警告: TInterfacedObject 有一个普通的构造函数,并且为了通过元类进行实例化以正常工作,您确实需要虚拟构造函数来确保调用正确的构造函数。就是将代码从后代中的构造函数覆盖移动到 AfterConstruction 覆盖。

笔记:

Rob 是绝对正确的,如果您需要在后代构造函数中进行特定于类的工作,则只需要虚拟构造函数。我的“建议”较少基于此示例中 TDetail 的具体细节,而更多基于使用虚拟构造函数“替换” TInterfacedObject 的优点,如果您通过依赖注入和模拟(或存根)来实现更多的控制反转。

笔记:

在上游不做任何更改的情况下让假货正常工作将是一项艰巨的任务。但是,您可能会采用使用 Delphi 范围顺序的方法:将您的测试存根(或模拟)命名为与您的测试类完全相同,并确保模拟单元在创建 TDetail 实例的单元的范围内比创建 TDetail 实例的单元的范围更近。包含实际定义的单元:

uses 
  Client, Detail, MockClient, MockDetail;

将确保TDetail.Create从 MockDetail 单元而不是 Detail 单元实例化 TDetail。

于 2012-12-05T19:58:08.503 回答