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.