如何模拟另一个类负责实例化的类?

时间:2011-05-02 13:27:00

标签: delphi unit-testing design-patterns dependency-injection mocking

请考虑以下代码:

type
  TFoo1 = class
  public
    procedure DoSomething1;
  end;

  TFoo2 = class
  private
    oFoo1 : TFoo1;
  public
    procedure DoSomething2;
    procedure DoSomething3;
    constructor Create;
    destructor Destroy; override;
  end;


procedure TFoo1.DoSomething1;
begin
  ShowMessage('TFoo1');
end;

constructor TFoo2.Create;
begin
  oFoo1 := TFoo1.Create;
end;

destructor TFoo2.Destroy;
begin
  oFoo1.Free;
  inherited;
end;

procedure TFoo2.DoSomething2;
begin
  oFoo1.DoSomething1;
end;

procedure TFoo2.DoSomething3;
var
  oFoo1 : TFoo1;
begin
  oFoo1 := TFoo1.Create;
  try
    oFoo1.DoSomething1;
  finally
    oFoo1.Free;
  end;
end;

我正在为一个班级创建单元测试,我坚持下去。我的问题都是关于模拟对象和我应该使用的设计模式的最佳方法。我是单元测试的类不是由我创建的。

  1. 在下面的示例中,我需要模拟Foo1,因为它向我在单元测试期间无法调用的Web服务发送请求。但是Foo1是由TFoo2构造函数创建的,我无法模仿它。在这种情况下我该怎么办?我应该修改TFoo2构造函数以接受像这样的Foo1对象吗?

    constructor TFoo2.Create(aFoo1 : TFoo1)
    begin
      oFoo1 := aFoo1;
    end;
    

    是否有一种设计模式表明我们需要传递一个类所依赖的所有对象,如上例所示?

  2. 方法TFoo2.DoSomething3创建Foo1对象,然后释放它。我是否还应修改该代码以传递Foo1对象?

    procedure TFoo2.DoSomething3(aFoo1 : TFoo1);
    begin
      aFoo1 := aFoo1.DoSomething1;
    end;
    
  3. 是否有任何设计模式支持我提出的建议?如果是这样,我可以告诉我工作的公司的所有开发人员,我们需要遵循XXX模式,以便更容易进行单元测试。

1 个答案:

答案 0 :(得分:8)

如果您无法模仿TFoo1的创建,则无法模拟TFoo1。现在,TFoo2负责创建TFoo1的所有实例,但如果这不是TFoo2的主要目的,那么这确实会使单元测试变得困难。

正如您所建议的,一种解决方案是将TFoo2所需的TFoo1个实例传递给TFoo2。这可能会使已调用TFoo1方法的所有当前代码复杂化。另一种方法是对TFoo2提供工厂,它更适合单元测试。工厂可以像函数指针一样简单,也可以是整个类。在Delphi中,元类也可以作为工厂。在构建时将工厂传递给TFoo2,并且只要TFoo1需要TFoo2实例,它就可以调用工厂。

要减少对其余代码的更改,可以使TFoo1.DoSomething1构造函数中的factory参数具有默认值。然后您不必更改应用程序代码。只需更改单元测试代码即可提供非默认的工厂参数。

无论你做什么,你都需要让type TFoo1 = class procedure DoSomethign1; virtual; end; TFoo1Class = class of TFoo1; TFoo2 = class private oFoo1 : TFoo1; FFoo1Factory: TFoo1Class; public constructor Create(AFoo1Factory: TFoo1Class = nil); end; constructor TFoo2.Create; begin inherited Create; FFoo1Factory := AFoo1Factory; if not Assigned(FFoo1Factory) then FFoo1Factory := TFoo1; oFoo1 := FFoo1Factory.Create; end; 成为虚拟的,否则嘲笑将是徒劳的。

使用元类,您的代码可能如下所示:

TFoo1

现在,您的单元测试代码可以提供TFoo2的模拟版本,并在创建type TMockFoo1 = class(TFoo1) procedure DoSomething1; override; end; procedure TMockFoo1.DoSomething1; begin // TODO: Pretend to access Web service end; procedure TestFoo2; var Foo2: TFoo2; begin Foo2 := TFoo2.Create(TMockFoo1); end; 时传递它:

TMockFoo1

元类的许多示例都为基类提供了一个虚拟构造函数,但这并不是绝对必要的。如果需要虚拟调用构造函数,则只需要一个虚拟构造函数 - 如果后代构造函数需要使用基类尚未执行的构造函数参数。如果后代(在这种情况下为AfterConstruction)与其祖先完成所有相同的事情,则构造函数不需要是虚拟的。 (还要记住{{1}}已经是虚拟的,这是让后代在不需要虚拟构造函数的情况下进行额外操作的另一种方法。)