2013-03-23 75 views
9

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

的“创建”方法

显然,这是因为下面的代码不会传递任何参数的“调用”方法:

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

我创建了一个重载类功能是否通过参数:

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

并正在进行有限的测试我已经完成。

我的问题是:

这是一个bug或我只是做错了?

感谢

PS:我知道,德尔福嘲笑为界面为中心,但它不支持类和代码库我工作的是99层%的类。

+1

这是我不明白。如果你想模拟一个类,为什么你想要创建一个你正在嘲笑的类的实例。当然,嘲笑的全部意义在于,你嘲笑课堂。 – 2013-03-23 14:18:46

+1

当您执行'TMock 。创建'Mocks框架将创建'TFoo'的实例。也许我不明白嘲笑,但我认为整个观点是你创造了一些不是'TFoo'的东西。我的意思是,如果你需要做的就是创建'TFoo',那么就做吧。如果你想嘲笑它,然后找到一个框架,将创建一个'TFoo'的模拟,而不是'TFoo'的一个实例。 – 2013-03-23 14:47:09

+0

@David。我很抱歉,我的问题跳到我的问题没有任何背景;你是对的。我想模拟一个构造函数有一个参数的类。正如在Delphi-Mocks项目展示[TesTObjectMock示例](https://github.com/VSoftTechnologies/Delphi-Mocks/blob/master/Sample1Main)上提供的示例一样。pas)被测试的类(TFoo)作为通用参数传递,如同mock:= TMock .create。问题在于类函数“Create”,它调用“Invoke”。 – TDF 2013-03-23 14:53:52

回答

6

正如我所看到的,根本问题是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; 

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

请让我知道,如果我在这里广泛的标志,并错过了这一点。这完全有可能!

+0

首先...哇,只是哇!我非常感谢你为此付出的努力。我一直对SO社区感到惊叹。谢谢。所以指的是原来的问题,这很可能是一个错误?如果是这样,我想提交你的解决方案的项目(经过广泛的测试课程)。你还好吗? – TDF 2013-03-23 17:22:23

+0

@TDF那么,我不太了解Delphi mock的设计,知道它是否是一个bug。我当然不想提出这样的建议。我建议你联系作者。作者最了解设计。这是一个非常有趣的问题和话题。顺便说一句,你有足够的声望能够投票。您可以投票以及接受。我当然认为你在这里得到应得票数的答案。 – 2013-03-23 17:25:21

+0

@DavideHeffernan我确实接受了你的回答并投票给它。你是否认为,作为一种礼节,我会投其他贡献者?他们肯定帮助解决了这个问题,我只是对这种习俗一无所知。谢谢 – TDF 2013-03-23 17:32:15

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的单一继承。

+0

这不是泛型。该代码使用RTTI调用构造函数。它假定被命名为“创建”。如果代码想要在调用'TRttiMethod'实例的'Invoke'时很容易传递参数。 – 2013-03-23 14:17:30

+0

@DavidHeffernan Aha。但即使不是泛型,框架如何知道参数和通过什么值? Rtti可以确定参数的数量和类型,但框架仍然不知道它们的含义。如果你希望框架实例化类,你需要“泛型”构造函数:无参数的构造函数和/或接收TValue(或变体)数组的函数;或者你必须在模拟框架上有一个通用的方法来为其提供一个TValue数组,以便按照规范的顺序将其传递给构造函数的特定参数。 – 2013-03-23 15:00:01

+0

您只需将参数传递给'TMock .Create'。一个开放的'TValue'数组。但对我来说,我不明白你为什么要用模拟来创建真实课堂的一个实例。这对我来说毫无意义。 – 2013-03-23 15:01:25

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; 

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

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

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

+0

我可以看到的基本问题是CUT最终被实例化。当然,这正是我们首先想要避免的。当然,你提出的建议将在这个框架的范围内完整地允许代码编译和运行。 – 2013-03-23 15:59:58

相关问题