10

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.

Guillem Vicens
  • 3,936
  • 30
  • 44
TDF
  • 125
  • 7
  • 1
    Here's what I don't understand. If you are trying to mock a class, why do you want an instance of the class that you are mocking to be created. Surely the whole point of mocking is that you, well, mock the class. – David Heffernan Mar 23 '13 at 14:18
  • 1
    When you do `TMock.Create` the Mocks framework creates an instance of `TFoo`. Perhaps I don't understand mocks, but I thought the whole point was that you created something that wasn't `TFoo`. I mean, if all you need to do is create `TFoo`, then just do it. If you want to mock it, then find a framework that will create a mock of `TFoo` rather than an instance of `TFoo`. – David Heffernan Mar 23 '13 at 14:47
  • @David. I'm sorry my question jumps right to my problem without any background; You are correct. I do want to mock a class whose constructor has a parameter(s). As the sample provided on the Delphi-Mocks project show [TesTObjectMock sample](https://github.com/VSoftTechnologies/Delphi-Mocks/blob/master/Sample1Main.pas) the class under test (TFoo) is passed as the generic parameter as in mock := TMock.create. The problem is in the class function "Create" and it calls "Invoke". – TDF Mar 23 '13 at 14:53
  • You can see for yourself that `TMock.Create` results in a call to `TFoo.Create`. So the conclusion that I draw is that you are meant to use an abstract base class and in that case you don't need parameters on the constructor since you never instantiate that base class. – David Heffernan Mar 23 '13 at 14:59
  • @DavidHeffernan, the purpose of mocks is to have something that looks acts and like the class under test (CUT) but allows you (the writer of the test) full control of what values the CUT can access or "see" when a test is invoked. Delphi-Mocks takes advantage of RTTI methods classes introduced in D2010(?) TVirtualMethodInterceptor in particular and (as I understand it) literally "intercepts" (or provides hooks to) all virtual methods. So I'm assuming that when ctor.Invoke is called, the actual class is in some way instantiated...? Instantiation would be bad. – TDF Mar 23 '13 at 15:17
  • Your use just enlight the fact that mocks and stubs are mainly to be used with factories and interfaces. You are for sure breaking the SOLID principles (liskov). – Arnaud Bouchez Mar 23 '13 at 15:49

3 Answers3

9

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!

David Heffernan
  • 601,492
  • 42
  • 1,072
  • 1,490
  • First off...WOW, just WOW! I really appreciate the work you've put into this. I'm continually amazed with the SO community. Thank you. So referring to the original question, this is most likely a bug? If so I'd like to submit your solution to the project (after extensive testing of course). Is that okay with you? – TDF Mar 23 '13 at 17:22
  • @TDF Well, I don't know enough about the design of Delphi mocks to know whether it's a bug. I would certainly not like to suggest that. I do suggest that you contact the author. The author knows the design best of all. It's a very interesting question and topic. By the way you have enough reputation to be able to up vote. You can vote as well as accept. I certainly think you have answers here that deserve up votes. – David Heffernan Mar 23 '13 at 17:25
  • @DavideHeffernan I did accept your answer and up vote it. Are you suggesting that as a matter of etiquette I up vote the other contributors? They've certainly helped with the issue, I'm just ignorant of this custom. Thanks – TDF Mar 23 '13 at 17:32
  • I can see that you are relatively new and so am trying to pass on some of these customs. I would not up vote out of etiquette. Only up vote if it is a good answer. I think Uwe's answer is worthy. – David Heffernan Mar 23 '13 at 17:33
  • @DavideHefferan Thank you for the nudge...done and done. I will be testing Uwe's and your solution further and will posts my findings here as well as contacting the author of Delphi-Mocks. Thanks again. – TDF Mar 23 '13 at 17:39
  • 1
    You actually don't need the enhanced RTTI to find the NewInstance method. You can do it like this: GetTypeData(TypeInfo(T)).ClassType.NewInstance – Stefan Glienke Mar 25 '13 at 07:33
  • @Stefan Thanks. I thought that was so, but my attempts failed. So I took the easy way out. This is definitely not part of Delphi that I am expert in. – David Heffernan Mar 25 '13 at 07:38
  • @DavidHeffernan It's my favourite part of Delphi :) I just fixed a similar bug in DSharp mocks. There it always called the Create of TObject (to create the instance) but the Destroy of the mocked class (because it's virtual) so that could have caused some AV or similar. – Stefan Glienke Mar 25 '13 at 07:45
  • @StefanGlienke What do you think of this answer? Given that this is not my specialist field (not by a long way). And given that you know so much more. Am I on the right track here? Or am I barking up the wrong tree. – David Heffernan Mar 25 '13 at 07:54
  • @DavidHeffernan Yes, you were right. Afaik the TVirtualMethodInterceptor needs an instance of the class it is intercepting (as intercepting does not mean faking the call at all but just tracking the before/after/onexception of the call). Using it for a mock would not require having an instance in theory as the instance itself is not called anymore (OnBefore says "no"). – Stefan Glienke Mar 25 '13 at 08:24
  • P.S. When you do not have an instance of the class (fake or not) the virtual method calls will fail (AV at address 00000000). – Stefan Glienke Mar 25 '13 at 08:36
  • @StefanGlienke I think the bare minimum that you need is for the *instance* to have a VMT. That VMT is modified by the VMI proxify. – David Heffernan Mar 25 '13 at 09:02
3

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.

Uwe Raabe
  • 45,288
  • 3
  • 82
  • 130
  • The fundamental problem that I can see is that the CUT ends up being instantiated. Surely that's what we are trying to avoid in the first place. Of course, what you propose will neatly allow the code to compile and run, within the confines of this framework. – David Heffernan Mar 23 '13 at 15:59
0

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.

Marjan Venema
  • 19,136
  • 6
  • 65
  • 79
  • It's not down to generics. The code uses RTTI to call the constructor. Which it assumes is named `Create`. If the code wanted to it could pass parameters quite easily when calling `Invoke` on the `TRttiMethod` instance. – David Heffernan Mar 23 '13 at 14:17
  • @DavidHeffernan Aha. But even if it isn't down to generics how would the framework know what parameters and what values to pass? Rtti can determine the number and types of parameters, but the framework still wouldn't have a clue as to the meaning of them. If you want the framework to instantiate classes, you need "generic" constructors: a parameterless one and/or one that receives an array of TValue (or Variant); or you have to have a generic method on the mock framework to supply it with an array of TValue to pass in order of specification into a constructor's specific parameters. – Marjan Venema Mar 23 '13 at 15:00
  • You'd just pass the parameters to `TMock.Create`. An open array of `TValue`. But to me I cannot understand why you'd want to use mocks to create an instance of the real class. That makes no sense to me. – David Heffernan Mar 23 '13 at 15:01
  • 1
    @David Not to make an instance of the real class but of its mock. And if the real class has constructor parameters, you may need some way of ensuring that the mock can take them as well. Mind you, I don't use mocking (but stubbing) so I may be way off base. However, both are very much dependent on dependency injection (where a framework that doesn't want the added burden of enhanced RTTI would also require parameterless constructors). If you don't have DI, the classes under test c/would be creating instances of what you want to mock/stub out. And if you then have specific constructors... – Marjan Venema Mar 23 '13 at 15:20
  • @DavidHeffernan "pass parameters quite easily when calling Invoke on the TRttiMethod" but that's the problem. The class function Create doesn't allow for parameters. I agree that we don't want to instantiate the CUT but if the constructor has a parameter, an exception is raised. – TDF Mar 23 '13 at 15:22
  • @TDF The bottom line is that you are instantiating the CUT. You need to stop doing that. – David Heffernan Mar 23 '13 at 15:29
  • @DavidHeffernan I appreciate you taking the time to look at this but given this code from the examples: procedure TesTObjectMock; var mock : TMock; begin mock := TMock.Create;, If the TFoo constructor has arguments how am I instantiating the CUT??? – TDF Mar 23 '13 at 15:41
  • @TDF Well, the framework instantiates CUT. That's what `ctor.Invoke` does. Give me a few minutes more, and I should have an answer for you. – David Heffernan Mar 23 '13 at 15:41
  • 1
    @MarjanVenema thank you. As I am working with a "somewhat" legacy code base, this is the case and I do have specific classes with constructors requiring arguments. – TDF Mar 23 '13 at 15:44