delphiinterfacemockingspring4d

Delphi Spring Mocking: Invalid Cast at `as` operation -- How do I solve this?


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.

Solution

  • 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: