使用Spring4D模拟方法时出现疑似内存泄漏问题
问题:Spring4D模拟IFileManager时FastMM检测到TIndexWrapper内存泄漏
我有一个类TMyClass,其中注入了接口IFileManager。该接口包含方法GetCompanyWorkbook(const ACompanyId: System.Integer; const AStream: TStream),此方法会根据ACompanyId填充AStream。真实代码运行一切正常,但在使用Spring4D框架对TMyClass进行单元测试并模拟IFileManager时,FastMM报告存在内存泄漏:13-20 bytes: TIndexWrapper x 1。我使用的是仓库main/master分支上的最新Spring4D 1.26版本。
以下是完整代码:
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;
问题原因与修复方案
核心原因
TIndexWrapper是Spring4D内部用于处理参数索引的辅助类,泄漏源于Mock的设置逻辑:
- 测试中同时使用
When(Args.Any)和方法参数匹配,导致内部创建的索引包装对象未被正确释放 - 匿名函数对类成员
FMockStream的引用,可能干扰Spring4D的Mock资源生命周期管理 - 代码中存在笔误:
TMyClass.GenerateInvoice里调用的GetComparyWorkbook应为GetCompanyWorkbook(少了一个'n'),虽不直接导致泄漏,但可能影响测试逻辑
修复步骤
1. 修正方法名拼写错误
procedure TMyClass.GenerateInvoice(const ACompanyId: System.Integer); begin var sTmpFile := Self.GetTempInvoiceFile(ACompanyId); var fs := TFileStream.Create(sTmpFile, fmCreate); try Self.FFileManager.GetCompanyWorkbook(ACompanyId, fs); // 修正拼写错误 // Do some operations with stream finally fs.Free(); end; end;
2. 调整Mock设置逻辑,移除冗余匹配条件
procedure TMyClassTests.SetupFileManagerMock(); begin Self.FFileManager.Setup.Executes( function(const callInfo: TCallInfo): TValue begin callInfo.Args[1].AsType<TStream>.CopyFrom(Self.FMockStream); Self.FMockStream.Position := 0; // 重置流位置,确保后续可重复读取 end) .GetCompanyWorkbook(Arg.IsAny<System.Integer>, Arg.IsAny<TStream>); // 移除.When(Args.Any),避免重复匹配逻辑导致的资源泄漏 end;
3. 显式重置Mock释放内部资源
在TearDown方法中添加Mock重置操作,确保内部辅助对象被正确释放:
procedure TMyClassTests.TearDown(); begin Self.FMyClass.Free(); Self.FMockStream.Free(); Self.FFileManager.Reset; // 显式重置Mock,释放内部资源 end;
备选方案:使用局部变量传递流对象
将MockStream改为测试方法内的局部变量,减少对类成员的引用,帮助Spring4D正确管理生命周期:
procedure TMyClassTests.TestGenerateInvoice(const ACompanyId: System.Integer; const AMockFile: string); var mockStream: TStream; begin mockStream := TFileStream.Create(AMockFile, fmOpenRead); try Self.FFileManager.Setup.Executes( function(const callInfo: TCallInfo): TValue begin callInfo.Args[1].AsType<TStream>.CopyFrom(mockStream); mockStream.Position := 0; end) .GetCompanyWorkbook(Arg.IsAny<System.Integer>, Arg.IsAny<TStream>); Assert.WillNotRaiseAny( procedure begin Self.FMyClass.GenerateInvoice(ACompanyId); end ); finally mockStream.Free; end; end;
内容的提问来源于stack exchange,提问作者urkainian
相关产品推荐
相关产品推荐

