I want to test a method where an interface is cast to another interface. The cast is valid since the one interface is derived by the other. Unfortunately I receive an error at the marked line. I allready tried to mock QueryInterface but the function was not called and the error was still there. Is there any possibility to handle this within Spring.Mocking
?
Tanks and keep healthy
unit Main;
{$M+}
interface
procedure Execute;
implementation
uses
Spring.Mocking,
System.SysUtils;
type
TRefFunc = reference to function: Boolean;
IHelper = interface
['{7950E166-1C93-47E4-8575-6B2CCEE05304}']
end;
IIntfToMock = interface
['{8D85A1CD-51E6-4135-B0E9-3E732400BA25}']
function DoSth(const AHelper: IHelper; const ARef: TRefFunc): Boolean;
end;
IYetAnotherIntf = interface(IIntfToMock)
['{95B54D3B-F573-4957-BDB3-367144270C3B}']
end;
IIntfProvider = interface
['{8B3E4B7B-1B2D-4E1F-942D-7E6EB4B9B585}']
function YetAnotherIntfFactory: IYetAnotherIntf;
end;
TClassToTest = class
private
FIntfProvider: IIntfProvider;
public
function MethodeToTest: Boolean;
constructor Create(const AIntfProvider: IIntfProvider);
end;
procedure Execute;
var
IntfMock : Mock<IIntfToMock>;
YetiMock : Mock<IYetAnotherIntf>;
ProvMock : Mock<IIntfProvider>;
Instance : TClassToTest;
OutObj : Pointer;
begin
IntfMock := Mock<IIntfToMock>.Create();
YetiMock := Mock<IYetAnotherIntf>.Create();
YetiMock.Setup.Returns(True).When.DoSth(Arg.IsAny<IHelper>, Arg.IsAny<TRefFunc>());
{
// Just a try. Did not work...
YetiMock.Setup.Executes(
function (const ACallInfo: TCallInfo): TValue
begin
ACallInfo.Args[1].From(IIntfToMock(IntfMock));
Result := TValue.From(True);
end
).When.QueryInterface(IIntfToMock, OutObj);
}
ProvMock := Mock<IIntfProvider>.Create();
ProvMock.Setup.Returns(TValue.From(IYetAnotherIntf(YetiMock))).When.YetAnotherIntfFactory;
Instance := TClassToTest.Create(ProvMock);
if Instance.MethodeToTest then
System.Writeln('everything works fine :)')
else
System.Writeln('that´s bad :(');
end;
{ TClassToTest }
constructor TClassToTest.Create(const AIntfProvider: IIntfProvider);
begin
Self.FIntfProvider := AIntfProvider;
end;
function TClassToTest.MethodeToTest: Boolean;
var
Instance : IIntfToMock;
YetAnother : IYetAnotherIntf;
begin
//
Result := False;
try
Instance := Self.FIntfProvider.YetAnotherIntfFactory;
Instance.DoSth(nil, nil);
YetAnother := Self.FIntfProvider.YetAnotherIntfFactory;
Instance := YetAnother; // works
Instance := IIntfToMock(YetAnother); // works
Instance := YetAnother as IIntfToMock; // BOOM: EIntfCastError
Result := True;
except
end;
end;
end.
Spring Mocks are more powerful than you think. A mock automatically returns a mock from methods returning a mockable interface (*) - and always the same instance of it. That means for factory mocks you don't need to specify any expectations. You just need to get hold of the mock returned to specify its behavior. Also found a small bug in there - it tries this on any interface regardless its "mockability" (is that a word? ^^). I will add a check here. Then in case it was not mockable the error will occur later if you really try to grab it as mock.
In order for a mock to also support other interfaces you simply have to tell it. This follows the same behavior as implementing interfaces in objects. If you only implement IYetAnotherIntf
in a class and store it in an interface variable of that type but then call as
, Supports
or QueryInterface
on it it will fail.
Here is the entire code - fwiw mocks are auto intialized so you don't have to call Create
which nicely reduces the code to its essence: the behavior specification.
Also if you don't care for any of the parameters at all you can write this a litte shorter.
procedure Execute;
var
ProvMock: Mock<IIntfProvider>;
YetiMock: Mock<IYetAnotherIntf>;
Instance: TClassToTest;
begin
// using type inference here - <IYetAnotherIntf> on From not necessary
YetiMock := Mock.From(ProvMock.Instance.YetAnotherIntfFactory);
// lets make the behavior strict here
// so it does not return False when there is no match
YetiMock.Behavior := TMockBehavior.Strict;
YetiMock.Setup.Returns(True).When(Args.Any).DoSth(nil, nil);
// this will internally add the IIntfToMock to the intercepted interfaces
// as it returns a Mock<IIntfToMock> we can also specify its behavior
// more about this particular case below
YetiMock.AsType<IIntfToMock>;
Instance := TClassToTest.Create(ProvMock);
if Instance.MethodeToTest then
System.Writeln('everything works fine :)')
else
System.Writeln('that´s bad :(');
end;
function TClassToTest.MethodeToTest: Boolean;
var
Helper: THelper;
RefFunc: TRefFunc;
Instance: IIntfToMock;
YetAnother: IYetAnotherIntf;
begin
Result := False;
try
// just using some variables for this demo
// to verify that arg matching is working
Helper := THelper.Create;
RefFunc := function: Boolean begin Result := False end;
Instance := FIntfProvider.YetAnotherIntfFactory;
Assert(Instance.DoSth(Helper, RefFunc));
YetAnother := FIntfProvider.YetAnotherIntfFactory;
Assert(YetAnother.DoSth(Helper, RefFunc));
// same as directly assign YetAnotherIntfFactory
Instance := YetAnother;
Assert(Instance.DoSth(Helper, RefFunc));
// same as before, direct assignment no interface cast via QueryInterface
Instance := IIntfToMock(YetAnother);
Assert(Instance.DoSth(Helper, RefFunc));
// QueryInterface "cast" - the interface interceptor internally needs to know
// that it also should handle that interface
Instance := YetAnother as IIntfToMock;
// the following also returns true currently but I think this is a defect
// internally setup for a mock returned via the AsType goes to the same
// interceptor and thus finds the expectation defined on the mock it was
// called on. That means you cannot specify derived behavior on such a mock
// or even worse if they are completely unrelated types but have identical
// methods they would interfer with each other - I will look into this
Assert(Instance.DoSth(Helper, RefFunc));
Result := True;
except
end;
end;
While preparing this answer I found the issue I described as I wanted to demonstrate that you can define a different behavior on the other interface just like you can when implementing interfaces in classes. As I wrote I will look into this any time soon. I think its a general missing feature on the interface interceptor as existing interceptors are just being passed to the additionally handled interface which is not desired here.
Update 12.04.2021: The two mentioned bugs are fixed now: