10

我开始使用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% 的类。

4

3 回答 3

8

在我看来,根本问题是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;

如您所见,代码假设您的类具有无参数构造函数。当你在你的类上调用 this 时,它的构造函数确实有参数,这会导致运行时 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;

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

请让我知道我是否在这里偏离了主题并且错过了重点。这是完全可能的!

于 2013-03-23T15:55:36.903 回答
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;

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

如果您确切知道何时实例化类,这也将起作用,以便您可以为类属性提供正确的参数。

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

于 2013-03-23T15:58:44.123 回答
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 的单一继承。

于 2013-03-23T09:29:43.817 回答