请考虑以下代码:
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;
我正在为一个班级创建单元测试,我坚持下去。我的问题都是关于模拟对象和我应该使用的设计模式的最佳方法。我是单元测试的类不是由我创建的。
在下面的示例中,我需要模拟Foo1
,因为它向我在单元测试期间无法调用的Web服务发送请求。但是Foo1
是由TFoo2
构造函数创建的,我无法模仿它。在这种情况下我该怎么办?我应该修改TFoo2
构造函数以接受像这样的Foo1
对象吗?
constructor TFoo2.Create(aFoo1 : TFoo1)
begin
oFoo1 := aFoo1;
end;
是否有一种设计模式表明我们需要传递一个类所依赖的所有对象,如上例所示?
方法TFoo2.DoSomething3
创建Foo1
对象,然后释放它。我是否还应修改该代码以传递Foo1
对象?
procedure TFoo2.DoSomething3(aFoo1 : TFoo1);
begin
aFoo1 := aFoo1.DoSomething1;
end;
是否有任何设计模式支持我提出的建议?如果是这样,我可以告诉我工作的公司的所有开发人员,我们需要遵循XXX模式,以便更容易进行单元测试。
答案 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}}已经是虚拟的,这是让后代在不需要虚拟构造函数的情况下进行额外操作的另一种方法。)