views:

215

answers:

2

I'm currently in the process of getting started with unit testing and mocking for good and I stumbled over the following method that I can't seem to fabricate a working mock implementation for:

function GetInstance(const AIID: TGUID; 
                       out AInstance; 
                     const AArgs: array of const; 
                     const AContextID: TImplContextID = CID_DEFAULT): Boolean;

(TImplContextID is just a type alias for Integer)

This is how far I got:

function TImplementationProviderMock.GetInstance(
  const AIID: TGUID;
    out AInstance;
  const AArgs: array of const;
  const AContextID: TImplContextID): Boolean;
var
  lCall: TMockMethod;
begin
  lCall := AddCall('GetInstance').WithParams([@AIID, AContextID]);
  Pointer(AInstance) := FindVarData(lCall.OutParams[0]).VPointer;
  Result := lCall.ReturnValue;
end;

But I haven't been able to figure out how I am supposed to mock the open array parameter AArgs. Any ideas?

Also, is there maybe a simpler way to to return the out-parameter AInstance and is using the @-notation for the TGUID-typed parameter (essentially a record, i.e. a value type) the right way to go?

Is it possible to mock this method with the current version of PascalMock at all?


Update 2: I have now cut down the question text for clarity. Originally it contained the following erroneous implementation of the mock method which was what Mason's reply refers to:

function TImplementationProviderMock.GetInstance(
  const AIID: TGUID;
    out AInstance;
  const AArgs: array of const;
  const AContextID: TImplContextID): Boolean;
begin
  Result := AddCall('GetInstance')
           .WithParams([@AIID, AContextID])
           .ReturnsOutParams([AInstance])
           .ReturnValue;
end;

In this the compiler complained about the .ReturnsOutParams([AInstance]) saying "Bad argument type in variable type array constructor.".

A: 

It looks like ReturnsOutParams expects an array of const, which is implemented internally as an array of TVarRec. TVarRec is a record that's sort of like a variant but different, and it requires a defined type for the compiler to fill it. An untyped parameter won't go into it.

This sort of thing could probably be done with Delphi 2010's extended RTTI, but not with TVarRec.

Mason Wheeler
I'm by now pretty sure that `ReturnsOutParams` is supposed to be called when defining the expectation rather than the mocked call. It doesn't really make sense otherwise. `TMockMethod` also has an array property called `OutParams` and I think that should be used to the return the values previously populated via `ReturnsOutParams`.
Oliver Giesen
That still leaves the question of how to mock the open array argument...
Oliver Giesen
@Oliver: Again, I don't think you can do that with a TVarRec. I'm no PascalMock expert, but it looks to me like this function is just too complex for the interface it provides.
Mason Wheeler
A: 

I've now come up with a somewhat elaborate solution which is not exactly ideal from an OO- POV because it requires that the implementer of the test knows how the mock is implemented internally but I think that is still somewhat acceptable as no assumption can be made about how else to specify an open array argument expectation anyway (at least none that would compile).

So, this is what the implementation of my mocked method now looks like:

function TImplementationProviderMock.GetInstance(
  const AIID: TGUID;
    out AInstance;
  const AArgs: array of const;
  const AContextID: TImplContextID): Boolean;
var
  lCall: TMockMethod;
  lArgs: TOpenArray;
begin
  lArgs := ConcatArrays([ArgsToArray([@AIID]), ArgsToArray(AArgs), ArgsToArray([AContextID])]);
  lCall := AddCall('GetInstance').WithParams(lArgs);
  Pointer(AInstance) := FindVarData(lCall.OutParams[0]).VPointer;
  Result := lCall.ReturnValue;
end;

As you can see the heart of my solution was to construct my own array of TVarRec (aka TOpenArray) which I could then pass to the WithParams-method. I wrote a couple of utility routines that allowed me to merge the explicit arguments with the open array arguments into a single new array.

Here is the implementation of ConcatArrays:

type TOpenArray = array of TVarRec;

function ConcatArrays(const AArrays: array of TOpenArray): TOpenArray;
var
  lLength: Integer;
  lArray: TOpenArray;
  lIdx: Integer;
  lElem: TVarRec;
begin
  lLength := 0;
  for lArray in AArrays do
    Inc(lLength, Length(lArray));
  SetLength(Result, lLength);
  lIdx := -1;
  for lArray in AArrays do
    for lElem in lArray do
      begin
        Inc(lIdx);
        Result[lIdx] := lElem;
      end;
end;

I do have a strong suspicion that these routines could probably be massively optimized by someone with a deeper understanding of how Delphi handles dynamic and open arrays internally.

Anyway, with this solution in place at the test site I now have to ignore the fact that there even is an open array parameter in the mocked method. I simply specify the expectation as such:

FMock.Expects('GetInstance').WithParams([@IMyIntf, 1, 2, 3, lContextID]).ReturnsOutParam(lDummy).Returns(True);

... where the 1, 2, 3-bit is really the expected open array argument.

Oliver Giesen