Search code examples
delphispring4ddunitx

Misleading memory leak on mocked method using Spring4D


I have a class TMyClass, on which I inject interface IFileManager. In this interface there is a method GetCompanyWorkbook(const ACompanyId: System.Integer; const AStream: TStream). This method fills AStream depend from ACompanyId. Everything works fine on real code, but when I run unit tests for class TMyClass and mocked IFileManager via framework Spring4D, FastMM reports for memory leak 13-20 bytes: TIndexWrapper x 1. I used last Spring4D version 1.26 from repository(branch main/master)

unit Unit1.pas

interface
  DUnitX.TestFramework,
  Spring.Mocking;

type
  IFileManager = interface (IInvokable)
   
   procedure GetCompanyWorkbook(const ACompanyId: System.Integer; const AStream: TStream);
  end;

  TMyClass = class
  strict private
    FFileManager: IFileManager;
  
  public
    constructor Create(const AFileManager: IFileManager);
    procedure GenerateInvoice(const ACompanyId: System.Integer);
  end;

  [TestFixture]
  TMyClassTests = class
  strict private
    FMockStream: TStream;
    FMyClass: TMyClass;
    FFileManager: Mock<IFileManager>;

    procedure SetupFileManagerMock();
    procedure InitMockStream(const AMockFile: string);

  public
    [Setup]
    procedure Setup();

    [TearDown]
    procedure TearDown();

    [TestCase('Test invoice generation', '2|invoice_2023.xls', '|')]
    procedure TestGenerateInvoice(const ACompanyId: System.Integer; const AMockFile: string);
  end;

implementation

uses
  System.Classes,
  Spring;

constructor TMyClass.Create(const AFileManager: IFileManager);
begin
  Guard.CheckNotNull(AFileManager, 'AFileManager');
  inherited Create();
  Self.FFileManager := AFileManager;
end;

procedure TMyClass.GenerateInvoice(const ACompanyId: System.Integer);
begin
  var sTmpFile := Self.GetTempInvoiceFile(ACompanyId);
  var fs := TFileStream.Create(sTmpFile, fmCreate);
  try
    Self.FFileManager.GetComparyWorkbook(ACompanyId, fs);
    // Do some operations with stream
  finally
    fs.Free();
  end;
end;

procedure TMyClassTests.Setup();
begin
  Self.FMockStream := nil;
  Self.FMyClass := TMyClass.Create(Self.FFileManager);
end;

procedure TMyClassTests.TearDown();
begin
  Self.FMyClass.Free();
  Self.FMockStream.Free();
end;

procedure TMyClassTests.InitMockStream(const AMockFile: string);
begin
  Self.FMockStream := TFileStream.Create(AMockFile, fmOpenRead);
end;

procedure TMyClassTests.SetupFileManagerMock();
begin
  Self.FFileManager.Setup.Executes(
    function(const callInfo: TCallInfo): TValue
    begin
      callInfo.Args[1].AsType<TStream>.CopyFrom(Self.FMockStream);
    end)
    .When(Args.Any)
    .GetCompanyWorkbook(Arg.IsAny<System.Integer>, Arg.IsAny<TStream>);
end;

procedure TMyClassTests.TestGenerateInvoice(const ACompanyId: System.Integer; const AMockFile: string);
begin
  Self.InitMockStream(AMockFile);
  Self.SetupFileManagerMock();
  Assert.WillNotRaiseAny(
    procedure
    begin
      Self.FMyClass.GenerateInvoice(ACompanyId);
    end
   );
end;

Solution

  • The issue is that you are using this construct which is redundant:

    .When(Args.Any)
    .GetCompanyWorkbook(Arg.IsAny<System.Integer>, Arg.IsAny<TStream>);
    

    Either pass Args.Any to When or use individual Arg matching on the parameters. Passing Args.Any causes the mock internally to ignore the individual parameter matching. That causes the temporarily constructed object for the parameter matching to be leaked which is not trivial to be fixed.

    Update: I was able to fix the memory leak in develop branch