Stubbing complex return values
Just imagine that the ForgotMyPassword method
as defined in previous articles does perform an internal test:
procedure TLoginController.ForgotMyPassword(const UserName: RawUTF8);
var U: TUser;
begin
U := fUserRepository.GetUserByName(UserName);
Assert(U.Name=UserName);
U.Password := Int32ToUtf8(Random(MaxInt));
if fSmsSender.Send('Your new password is '+U.Password,U.MobilePhoneNumber) then
fUserRepository.Save(U);
end;
This will fail the test for sure, since by default,
GetUserByName stubbed method will return a valid but void record.
It means that U.Name will equal '', so the
highlighted line will raise an EAssertionFailed exception.
Here is how we may enhance our stub, to ensure it will return a
TUser value matching U.Name='toto':
var UserRepository: IUserRepository;
U: TUser;
(...)
U.Name := 'toto';
TInterfaceMock.Create(TypeInfo(IUserRepository),UserRepository,self).
Returns('GetUserByName','"toto"',RecordSaveJSON(U,TypeInfo(TUser))).
ExpectsCount('Save',qoEqualTo,1);
The only trick in the above code is that we use
RecordSaveJSON() function to compute the internal JSON
representation of the record, as expected by mORMot's data
marshaling.
Stubbing via a custom delegate or callback
In some cases, it could be very handy to define a complex process for a given method, without the need of writing a whole implementation class.
A delegate or event callback can be specified to implement this process,
with three parameters marshaling modes:
- Via some Named[] variant properties (which are the default for
the Ctxt callback parameter) - the easiest and safest to work
with;
- Via some Input[] and Output[] variant
properties;
- Directly as a JSON array text (the fastest, since native to the
mORMot core).
Let's emulate the following behavior:
function TServiceCalculator.Subtract(n1, n2: double): double; begin result := n1-n2; end;
Delegate with named variant parameters
You can stub a method using a the Named[] variant arrays as
such:
TInterfaceStub.Create(TypeInfo(ICalculator),ICalc).
Executes('Subtract',IntSubtractVariant);
(...)
Check(ICalc.Substract(10.5,1.5)=9);
The callback function can be defined as such:
procedure TTestServiceOrientedArchitecture.IntSubtractVariant(
Ctxt: TOnInterfaceStubExecuteParamsVariant);
begin
Ctxt['result'] := Ctxt['n1']-Ctxt['n2'];
end;
That is, callback shall use Ctxt['..'] property to access the
parameters and result as variant values.
In fact, we use the Ctxt.Named[] default property, so it is
exactly as the following line:
Ctxt.Named['result'] := Ctxt.Named['n1']-Ctxt.Named['n2'];
If the execution fails, it shall execute Ctxt.Error() method
with an associated error message to notify the stubbing process of such a
failure.
Using named parameters has the advantage of being more explicit in case of change of the method signature (e.g. if you add or rename a parameter). It should be the preferred way of implementing such a callback, in most cases.
Delegate with indexed variant parameters
There is another way of implementing such a callback method, directly by
using the Input[] and Output[] indexed properties. It
should be (a bit) faster to execute:
procedure TTestServiceOrientedArchitecture.IntSubtractVariant(
Ctxt: TOnInterfaceStubExecuteParamsVariant);
begin
with Ctxt do
Output[0] := Input[0]-Input[1]; // result := n1-n2
end;
Just as with TOnInterfaceStubExecuteParamsJSON implementation,
Input[] index follows the exact order of const and
var parameters at method call, and Output[] index
follows the exact order of var and out parameters
plus any function result.
That is, if you call:
function Subtract(n1,n2: double): double; ... MyStub.Substract(100,20);
you have in TOnInterfaceStubExecuteParamsJSON:
Ctxt.Params = '100,20.5'; // at method call Ctxt.Result = '[79.5]'; // after Ctxt.Returns([..])
and in the variant arrays:
Ctxt.Input[0] = 100; // =n1 at method call Ctxt.Input[1] = 20.5; // =n2 at method call Ctxt.Output[0] = 79.5; // =result after method call
In case of additional var or out parameters, those
should be added to the Output[] array before the last one, which
is always the function result.
If the method is defined as a procedure and not as a
function, of course there is no last Output[] item,
but only var or out parameters.
Delegate with JSON parameters
You can stub a method using a JSON array as such:
TInterfaceStub.Create(TypeInfo(ICalculator),ICalc).
Executes('Subtract',IntSubtractJSON);
(...)
Check(ICalc.Substract(10.5,1.5)=9);
The callback shall be defined as such:
procedure TTestServiceOrientedArchitecture.IntSubtractJSON( Ctxt: TOnInterfaceStubExecuteParamsJSON); var P: PUTF8Char; begin // result := n1-n2 P := pointer(Ctxt.Params); Ctxt.Returns([GetNextItemDouble(P)-GetNextItemDouble(P)]); // Ctxt.Result := '['+DoubleToStr(GetNextItemDouble(P)-GetNextItemDouble(P))+']'; end;
That is, it shall parse incoming parameters from Ctxt.Params,
and store the result values as a JSON array in Ctxt.Result.
Input parameter order in Ctxt.Params follows the exact order of
const and var parameters at method call, and output
parameter order in Ctxt.Returns([]) or Ctxt.Result
follows the exact order of var and out parameters
plus any function result.
Accessing the test case when mocking
In case of mocking, you may add additional verifications within the implementation callback, as such:
TInterfaceMock.Create(TypeInfo(ICalculator),ICalc,self).
Executes('Subtract',IntSubtractVariant,'toto');
(...)
procedure TTestServiceOrientedArchitecture.IntSubtractVariant(
Ctxt: TOnInterfaceStubExecuteParamsVariant);
begin
Ctxt.TestCase.Check(Ctxt.EventParams='toto');
Ctxt['result'] := Ctxt['n1']-Ctxt['n2'];
end;
Here, an additional callback-private parameter containing
'toto' has been specified at TInterfaceMock
definition.
Then its content is checked on the associated test case via
Ctxt.Sender instance. If the caller is not a
TInterfaceMock, it will raise an exception to use the
Ctxt.TestCase property.
Calls tracing
As stated above, mORMot is able to log all interface calls into
internal TInterfaceStub's structures. This is indeed the root
feature of its "test spy" TInterfaceMockSpy.Verify() methods.
Stub := TInterfaceStub.Create(TypeInfo(ICalculator),I).
SetOptions([imoLogMethodCallsAndResults]);
Check(I.Add(10,20)=0,'Default result');
Check(Stub.LogAsText='Add(10,20)=[0]');
Here above, we retrieved the whole call stack, including returned results, as an easy to read text content.
A more complex trace verification could be defined for instance, in the context of an interface mock:
TInterfaceMock.Create(TypeInfo(ICalculator),I,self).
Returns('Add','30').
Returns('Multiply',[60]).
Returns('Multiply',[2,35],[70]).
ExpectsCount('Multiply',qoEqualTo,2).
ExpectsCount('Subtract',qoGreaterThan,0).
ExpectsCount('ToTextFunc',qoLessThan,2).
ExpectsTrace('Add',Hash32('Add(10,30)=[30]')).
ExpectsTrace('Multiply',Hash32('Multiply(10,30)=[60],Multiply(2,35)=[70]')).
ExpectsTrace('Multiply',[10,30],Hash32('Multiply(10,30)=[60]')).
ExpectsTrace(Hash32('Add(10,30)=[30],Multiply(10,30)=[60],'+
'Multiply(2,35)=[70],Subtract(2.3,1.2)=[0],ToTextFunc(2.3)=["default"]')).
Returns('ToTextFunc',['default']);
Check(I.Add(10,30)=30);
Check(I.Multiply(10,30)=60);
Check(I.Multiply(2,35)=70);
Check(I.Subtract(2.3,1.2)=0,'Default result');
Check(I.ToTextFunc(2.3)='default');
The ExpectsTrace() methods are able to add some checks non only
about the number of calls of a method, but the order of the command executions,
and the retrieved result values. Those methods expect Hash32()
functions to define the hash value of the expected trace, which is a good way
of minimizing data in memory or re-use a value retrieved at execution time for
further regression testing.
You have even a full access to the internal execution trace, via the two
TInterfaceStub.Log and LogCount properties. This will
allow any validation of mocked interface calls logic, beyond
ExpectsTrace() possibilities.
You can take a look at
TTestServiceOrientedArchitecture.MocksAndStubs regression tests,
for a whole coverage of all the internal features.
This article is part of a list of mocking/stubbing range of information:
- Interfaces in practice: dependency injection, stubs and mocks
- Stubs and Mocks for Delphi with mORMot;
- Advanced mocks and stubs;
Feedback is welcome on our forum, just as usual.
