4

请考虑下面的代码:如何模拟另一个类负责实例化的类?

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模式,以便使单元测试更容易。

回答

8

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

一种解决方案是,如你所说,通过它需要TFoo2任何TFoo1实例。这可能使已经调用TFoo2方法的所有当前代码复杂化。另一种方式是单元测试友好一点,就是为TFoo1提供工厂。工厂可以像函数指针一样简单,也可以是整个类。在德尔福,metaclass也可以作为工厂。在工厂建造时将工厂传递到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; 

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

+0

非常好,谢谢。元类思想非常酷。 – 2011-05-02 13:48:25

+0

你知不知道是否有任何设计模式告诉我们,我们需要通过构造函数或其他方法传递对象依赖关系?它是装饰者模式吗? – 2011-05-02 14:46:09

+4

你在找什么是依赖注入或控制反转。您已经在TFoo2上创建了TFoo1的依赖关系。相反,您想通过接口或构造函数将TFoo2“注入”到TFoo1中。请记住,如果你在编写单元测试时遇到困难,那么你做得不对。 ;-) – 2011-05-02 15:08:42

相关问题