Delphi-Mocks:在构造函数中使用参数模拟一个类

时间:2013-03-23 01:17:49

标签: delphi mocking delphi-mocks

我开始使用Delphi-Mocks框架,并且在模拟构造函数中具有参数的类时遇到问题。 TMock的类功能“Create”不允许参数。如果尝试创建TFoo.Create的模拟实例(Bar:someType);当TObjectProxy.Create时,我得到参数计数不匹配;试图调用T的“创建”方法。

显然这是因为以下代码没有将任何参数传递给“Invoke”方法:

instance := ctor.Invoke(rType.AsInstance.MetaclassType, []);

我创建了一个重载的类函数,它传入参数:

class function Create( Args: array of TValue ): TMock<T>; overload;static;

正在进行有限的测试。

我的问题是:

这是一个错误还是我做错了?

由于

PS:我知道Delphi-Mocks是以接口为中心的,但它确实支持类,我正在处理的代码库是99%的类。

3 个答案:

答案 0 :(得分:7)

正如我所看到的,基本问题是TMock<T>.Create导致被测试的类(CUT)被实例化。我怀疑框架的设计是假设你要模拟一个抽象基类。在这种情况下,实例化它将是良性的。我怀疑你正在处理遗留代码,它没有一个方便的CUT抽象基类。但在你的情况下,实例化CUT的唯一方法是将参数传递给构造函数,从而破坏了模拟的整个目的。我宁愿想象,在为所有需要模拟的类创建一个抽象基类之前,重新设计遗留代码库需要做很多工作。

您正在撰写TMock<TFoo>.Create,其中TFoo是一个类。这导致创建代理对象。这发生在TObjectProxy<T>.Create。代码如下所示:

constructor TObjectProxy<T>.Create;
var
  ctx   : TRttiContext;
  rType : TRttiType;
  ctor : TRttiMethod;
  instance : TValue;
begin
  inherited;
  ctx := TRttiContext.Create;
  rType := ctx.GetType(TypeInfo(T));
  if rType = nil then
    raise EMockNoRTTIException.Create('No TypeInfo found for T');

  ctor := rType.GetMethod('Create');
  if ctor = nil then
    raise EMockException.Create('Could not find constructor Create on type ' + rType.Name);
  instance := ctor.Invoke(rType.AsInstance.MetaclassType, []);
  FInstance := instance.AsType<T>();
  FVMInterceptor := TVirtualMethodInterceptor.Create(rType.AsInstance.MetaclassType);
  FVMInterceptor.Proxify(instance.AsObject);
  FVMInterceptor.OnBefore := DoBefore;
end;

正如您所看到的,代码假设您的类具有无参数构造函数。当你在你的类上调用它时,它的构造函数确实有参数,这会导致运行时RTTI异常。

当我理解代码时,该类仅为了拦截其虚拟方法而实例化。我们不想在课堂上做任何其他事情,因为这样做会挫败嘲笑它的目的。您真正需要的只是一个具有合适vtable的对象实例,可以由TVirtualMethodInterceptor操纵。您不需要或不希望您的构造函数运行。你只是希望能够模拟碰巧有一个带参数的构造函数的类。

所以代替调用构造函数的代码,我建议你修改它以使其调用NewInstance。这是您需要做的最低限度才能拥有可以操作的vtable。而且您还需要修改代码,以便它不会尝试销毁模拟实例,而是调用FreeInstance。只要你所做的只是在模拟上调用虚方法,所有这一切都会正常工作。

修改如下:

constructor TObjectProxy<T>.Create;
var
  ctx   : TRttiContext;
  rType : TRttiType;
  NewInstance : TRttiMethod;
  instance : TValue;
begin
  inherited;
  ctx := TRttiContext.Create;
  rType := ctx.GetType(TypeInfo(T));
  if rType = nil then
    raise EMockNoRTTIException.Create('No TypeInfo found for T');

  NewInstance := rType.GetMethod('NewInstance');
  if NewInstance = nil then
    raise EMockException.Create('Could not find NewInstance method on type ' + rType.Name);
  instance := NewInstance.Invoke(rType.AsInstance.MetaclassType, []);
  FInstance := instance.AsType<T>();
  FVMInterceptor := TVirtualMethodInterceptor.Create(rType.AsInstance.MetaclassType);
  FVMInterceptor.Proxify(instance.AsObject);
  FVMInterceptor.OnBefore := DoBefore;
end;

destructor TObjectProxy<T>.Destroy;
begin
  TObject(Pointer(@FInstance)^).FreeInstance;//always dispose of the instance before the interceptor.
  FVMInterceptor.Free;
  inherited;
end;

坦率地说,这看起来对我来说更为明智。调用构造函数和析构函数肯定没有意义。

请告诉我,如果我在这里的标记很广泛并且错过了重点。这完全有可能!

答案 1 :(得分:3)

我不确定我是否正确地满足了您的需求,但也许这种hacky方法可能有所帮助。假设你的类在构造函数中需要一个参数

type
  TMyClass = class
  public
    constructor Create(AValue: Integer);
  end;

您可以使用无参数构造函数和包含参数

的类属性继承此类
type
  TMyClassMockable = class(TMyClass)
  private
  class var
    FACreateParam: Integer;
  public
    constructor Create;
    class property ACreateParam: Integer read FACreateParam write FACreateParam;
  end;

constructor TMyClassMockable.Create;
begin
  inherited Create(ACreateParam);
end;

现在您可以使用class属性将参数传递给构造函数。当然,你必须将继承的类赋予mock框架,但是由于没有其他任何改变,派生类也应该这样做。

如果您确切知道实例化类的时间,那么这也只会起作用,因此您可以为类属性提供正确的参数。

不用说这种方法不是线程安全的。

答案 2 :(得分:0)

免责声明:我不了解Delphi-Mocks。

我想这是设计的。从您的示例代码看,Delphi-Mocks看起来像是使用泛型。如果要实例化泛型参数的实例,如:

function TSomeClass<T>.CreateType: T;
begin
  Result := T.Create;
end;

然后你需要一个泛型类的构造函数约束:

TSomeClass<T: class, constructor> = class

具有构造函数约束意味着传入的类型必须具有无参数构造函数。

你可能会做类似

的事情
TSomeClass<T: TSomeBaseMockableClass, constructor> = class

并为TSomeBaseMockableClass提供一个特定的构造函数,然后可以使用但是

要求你的框架的所有用户从特定的基类派生他们所有的类只是......好吧......过于严格(温和地说),特别是考虑到Delphi的单一继承。