【问题标题】:Delphi-Mocks: Mocking a class with parameters in the constructorDelphi-Mocks:在构造函数中模拟带有参数的类
【发布时间】:2013-03-23 01:17:48
【问题描述】:

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

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

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

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

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

并且正在使用我所做的有限测试。

我的问题是:

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

谢谢

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

【问题讨论】:

  • 这是我不明白的。如果您尝试模拟一个类,为什么要创建您正在模拟的类的实例。当然,嘲弄的全部意义在于你,嗯,嘲弄班级。
  • 当您执行TMock&lt;TFoo&gt;.Create 时,Mocks 框架会创建一个TFoo 的实例。也许我不理解模拟,但我认为重点在于您创建的东西不是TFoo。我的意思是,如果您只需要创建TFoo,那就去做吧。如果你想模拟它,那么找到一个框架来创建一个模拟 TFoo 而不是 TFoo 的实例。
  • @大卫。对不起,我的问题没有任何背景就直接跳到我的问题上;你是对的。我确实想模拟一个构造函数有参数的类。正如 Delphi-Mocks 项目中提供的示例显示 TesTObjectMock sample 一样,被测类 (TFoo) 作为通用参数传递,就像在 mock := TMock.create 中一样。问题出在类函数“Create”中,它调用了“Invoke”。
  • 您可以亲眼看到TMock&lt;TFoo&gt;.Create 导致调用TFoo.Create。所以我得出的结论是,您应该使用抽象基类,在这种情况下,您不需要构造函数上的参数,因为您从未实例化该基类。
  • @DavidHeffernan,模拟的目的是让某些东西看起来像被测类 (CUT),但允许您(测试的编写者)完全控制 CUT 可以访问的值或在调用测试时“查看”。 Delphi-Mocks 特别利用了 D2010(?) TVirtualMethodInterceptor 中引入的 RTTI 方法类,并且(据我理解)字面上“拦截”(或提供挂钩)所有虚拟方法。所以我假设当调用 ctor.Invoke 时,实际的类是以某种方式实例化的......?实例化会很糟糕。

标签: delphi mocking delphi-mocks


【解决方案1】:

在我看来,根本问题是TMock&lt;T&gt;.Create 导致被测类 (CUT) 被实例化。我怀疑该框架是在您将模拟抽象基类的假设下设计的。在这种情况下,实例化它是良性的。我怀疑您正在处理没有方便的 CUT 抽象基类的遗留代码。但是在您的情况下,实例化 CUT 的唯一方法涉及将参数传递给构造函数,因此破坏了模拟的整个目的。而且我更愿意想象,重新设计遗留代码库将需要做很多工作,直到你为所有需要模拟的类都有一个抽象基类。

您正在编写TMock&lt;TFoo&gt;.Create,其中TFoo 是一个类。这会导致创建一个代理对象。这发生在TObjectProxy&lt;T&gt;.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;

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

如果我在此范围内偏离了主题并错过了重点,请告诉我。这完全有可能!

【讨论】:

  • 首先...哇,哇!我非常感谢您为此付出的努力。我一直对 SO 社区感到惊讶。谢谢你。所以参考原始问题,这很可能是一个错误?如果是这样,我想将您的解决方案提交给项目(当然经过广泛的测试)。你没问题吗?
  • @TDF 好吧,我对 Delphi 模拟的设计了解得不够多,不知道这是否是一个错误。我当然不想这样建议。建议你联系作者。作者最了解设计。这是一个非常有趣的问题和话题。顺便说一句,您有足够的声誉可以投票。您可以投票也可以接受。我当然认为你在这里有值得投票的答案。
  • @DavideHeffernan 我确实接受了你的回答并投了赞成票。您是否建议出于礼节我投票支持其他贡献者?他们当然帮助解决了这个问题,我只是不知道这个习俗。谢谢
  • 我可以看到你是比较新的,所以我试图传递一些这些习俗。我不会出于礼节投票。只有当它是一个好的答案时才投赞成票。我认为 Uwe 的回答是值得的。
  • 您实际上不需要增强型 RTTI 来查找 NewInstance 方法。你可以这样做:GetTypeData(TypeInfo(T)).ClassType.NewInstance
【解决方案2】:

我不确定我是否正确地满足了您的需求,但也许这种 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;

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

这也只有在您确切知道类何时被实例化的情况下才有效,这样您就可以为类属性提供正确的参数。

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

【讨论】:

  • 我可以看到的根本问题是 CUT 最终被实例化了。当然,这就是我们首先要避免的。当然,您提出的建议将巧妙地允许代码在此框架的范围内编译和运行。
【解决方案3】:

免责声明:我不了解 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 的单一继承。

【讨论】:

  • 这不取决于泛型。代码使用 RTTI 调用构造函数。它假定命名为Create。如果代码愿意,它可以在 Invoke 实例上调用 Invoke 时很容易地传递参数。
  • @DavidHeffernan 啊哈。但即使不是泛型,框架如何知道要传递哪些参数和值? Rtti 可以确定参数的数量和类型,但框架仍然不知道它们的含义。如果您希望框架实例化类,则需要“通用”构造函数:无参数的构造函数和/或接收 TValue(或 Variant)数组的构造函数;或者您必须在模拟框架上有一个通用方法来为其提供一个 TValue 数组,以便按照规范的顺序传递给构造函数的特定参数。
  • 您只需将参数传递给TMock&lt;TFoo&gt;.CreateTValue 的开放数组。但对我来说,我不明白你为什么要使用模拟来创建真实类的实例。这对我来说毫无意义。
  • @David 不要创建真实类的实例,而是创建它的模拟类。如果真正的类有构造函数参数,你可能需要一些方法来确保模拟也可以接受它们。请注意,我不使用嘲笑(但存根),所以我可能会偏离基础。但是,两者都非常依赖依赖注入(其中不希望增加增强 RTTI 负担的框架也需要无参数构造函数)。如果您没有 DI,则测试中的类 c/ 将创建您想要模拟/存根的实例。如果你有特定的构造函数......
  • @MarjanVenema 谢谢。当我使用“有点”的遗留代码库时,情况就是这样,并且我确实有特定的类,其构造函数需要参数。
猜你喜欢
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2012-04-28
  • 1970-01-01
  • 1970-01-01
  • 2011-05-07
  • 2017-01-21
  • 1970-01-01
相关资源
最近更新 更多