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

Raf*_*cci 4 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;
Run Code Online (Sandbox Code Playgroud)

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

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

    constructor TFoo2.Create(aFoo1 : TFoo1)
    begin
      oFoo1 := aFoo1;
    end;
    
    Run Code Online (Sandbox Code Playgroud)

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

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

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

Rob*_*edy 8

如果你不能嘲笑创作TFoo1,那你就不能嘲笑TFoo1.现在,TFoo2负责创建所有实例TFoo1,但如果这不是主要目的TFoo2,那么这确实会使单元测试变得困难.

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

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

无论你做什么,你都需要TFoo1.DoSomething1虚拟,否则嘲笑将是徒劳的.

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

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;
Run Code Online (Sandbox Code Playgroud)

现在,您的单元测试代码可以提供模拟版本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;
Run Code Online (Sandbox Code Playgroud)

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

  • 您正在寻找的是依赖注入或控制反转.您已经在TFoo2上创建了TFoo1的依赖项.相反,您希望通过接口或构造函数将TFoo2"注入"TFoo1.请记住,如果您在编写单元测试时遇到困难,那么他们就是你做得不对.;-) (4认同)