【问题标题】:How do I mock a class that another class is responsible for instantiating?如何模拟另一个类负责实例化的类?
【发布时间】:2011-05-02 13:26:59
【问题描述】:

请考虑以下代码:

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 模式,以便更轻松地进行单元测试。

【问题讨论】:

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


    【解决方案1】:

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

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

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

    无论做什么,都需要将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;
    

    现在,您的单元测试代码可以提供 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 已经是虚拟的,所以这是另一种让后代执行额外操作而无需虚拟构造函数的方法。)

    【讨论】:

    • 你知道是否有任何设计模式告诉我们需要通过构造函数或其他方式传递对象依赖关系?是装饰器模式吗?
    • 您正在寻找的是依赖注入或控制反转。您已经在 TFoo2 上创建了 TFoo1 的依赖项。相反,您希望通过接口或构造函数将 TFoo2 “注入”到 TFoo1 中。请记住,如果您在编写单元测试时遇到困难,那么您就没有做对。 ;-)
    • +1:我还建议查看一些 Misko Hevery 的文章和 You Tube 视频。他会给你一些很棒的想法,让你的代码更容易测试。例如:Clean Code Talks Dependency Injection
    • @Craig 非常好的建议。谢谢
    • @尼克。是的,这正是我要找的名字。
    猜你喜欢
    • 2016-05-13
    • 2020-06-25
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多