Question

I'm starting to use the Delphi-Mocks framework and am having trouble with mocking a class that has parameters in the constructor. The class function "Create" for TMock does not allow parameters. If try to create a mock instance of TFoo.Create( Bar: someType ); I get a Parameter count mismatch' when TObjectProxy.Create; attempts to call the 'Create' method of T.

Clearly this is because the following code does not pass any parameters to the "Invoke" method:

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

I've created an overloaded class function that DOES pass in parameters:

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

and is working with the limited testing I've done.

My question is:

Is this a bug or am I just doing it wrong?

Thanks

PS: I know that Delphi-Mocks is Interface-centric but it does support classes and the code base I'm working on is 99% Classes.

Was it helpful?

Solution

The fundamental issue, as I see it, is that TMock<T>.Create results in the class under test (CUT) being instantiated. I suspect that the framework was designed under the assumption that you would mock an abstract base class. In which case, instantiating it would be benign. I suspect that you are dealing with legacy code which does not have a handy abstract base class for the CUT. But in your case, the only way to instantiate the CUT involves passing parameters to the constructor and so defeats the entire purpose of mocking. And I rather imagine that it's going to be a lot of work to re-design the legacy code base until you have an abstract base class for all classes that need to be mocked.

You are writing TMock<TFoo>.Create where TFoo is a class. This results in a proxy object being created. That happens in TObjectProxy<T>.Create. The code of which looks like this:

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;

As you can see the code makes an assumption that your class has a no parameter constructor. When you call this on your class, whose constructor does have parameters, this results in a runtime RTTI exception.

As I understand the code, the class is instantiated solely for the purpose of intercepting its virtual methods. We don't want to do anything else with the class since that would rather defeat the purpose of mocking it. All you really need is an instance of an object with a suitable vtable that can be manipulated by TVirtualMethodInterceptor. You don't need or want your constructor to run. You just want to be able to mock a class that happens to have a constructor that has parameters.

So instead of this code calling the constructor I suggest you modify it to make it call NewInstance. That's the bare minimum that you need to do in order to have a vtable that can be manipulated. And you'll also need to modify the code so that it does not attempt to destroy the mock instance and instead calls FreeInstance. All this will work fine so long as all you do is call virtual methods on the mock.

The modifications look like this:

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;

Frankly this looks a bit more sensible to me. There's surely no point in calling constructors and destructors.

Please do let me know if I'm wide of the mark here and have missed the point. That's entirely possible!

OTHER TIPS

I'm not sure if I correctly get your needs, but perhaps this hacky approach might help. Assuming you have a class that needs a parameter in its constructor

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

you can inherit this class with a parameterless constructor and a class property that holds the parameter

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;

Now you can use the class property to transfer the parameter to the constructor. Of course you have to give the inherited class to the mock framework, but as nothing else changed the derived class should do as well.

This will also only work, if you know exactly when the class is instantiated so you can give the proper parameter to the class property.

Needless to say that this approach is not thread safe.

Disclaimer: I have no knowledge of Delphi-Mocks.

I guess this is by design. From your sample code it looks like Delphi-Mocks is using generics. If you want to instantiate an instance of a generic parameter, as in:

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

then you need a constructor constraint on the generic class:

TSomeClass<T: class, constructor> = class

Having a constructor constraint means that the passed in type must have parameter-less constructor.

You could probably do something like

TSomeClass<T: TSomeBaseMockableClass, constructor> = class

and give TSomeBaseMockableClass a specific constructor as well that could then be used, BUT:

Requiring all users of your framework to derive all their classes from a specific base class is just ... well ... overly restrictive (to put it mildly) and especially so considering Delphi's single inheritance.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top