Delphi Spring Mocking:在 `as` 操作中无效的转换 -- 我该如何解决这个问题?

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

我想测试将一个接口强制转换为另一个接口的方法。转换是有效的,因为一个接口是由另一个接口派生的。不幸的是,我在标记的行收到错误。我已经尝试模拟 QueryInterface 但未调用该函数并且错误仍然存​​在。有没有可能在 Spring.Mocking 内处理这个问题?

坦克并保持健康

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 模拟比你想象的更强大。 模拟自动 return 从方法 returning 一个可模拟接口 (*) 中模拟 - 并且始终是它的相同实例。这意味着对于工厂模拟,您不需要指定任何期望。您只需要获取 mock returned 来指定它的行为。 还在那里发现了一个小错误 - 它会在任何界面上尝试此操作,而不管其“可模拟性”(这是一个词吗?^^)。我将在这里添加一张支票。然后,如果它不是可模拟的,那么如果您真的尝试将其作为模拟来抓取,稍后会发生错误。

为了让 mock 也支持其他接口,您只需告诉它即可。这遵循与在对象中实现接口相同的行为。如果您仅在 class 中实现 IYetAnotherIntf 并将其存储在该类型的接口变量中,然后在其上调用 asSupportsQueryInterface,它将失败。

这是完整的代码 - fwiw 模拟是自动初始化的,因此您不必调用 Create,这很好地减少了代码的本质:行为规范。 另外,如果你根本不关心任何参数,你可以把它写得更短一些。

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;

在准备这个答案时,我发现了我描述的问题,因为我想证明您可以在另一个接口上定义不同的行为,就像在 classes 中实现接口一样。正如我所写的那样,我将很快对此进行调查。我认为它是接口拦截器上普遍缺少的功能,因为现有的拦截器只是被传递给额外处理的接口,这在此处是不需要的。

2021 年 4 月 12 日更新:上述两个错误现已修复:

  • 方法return只有当接口有方法信息
  • 时,接口才会自动return模拟
  • 在 mock 上支持其他接口时,每个接口都有自己的行为规范