Search code examples
delphimockingdelphi-mocks

Delphi-Mocks: Mocking a class with parameters in the constructor


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.


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!