如何用DUnitX测试无构造函数的Delphi类?
问题分析与解决方案
一、当前类结构的可测试性问题
DoSomething是私有方法,无法直接在测试用例中调用,只能通过Execute触发,但Execute内部实例化的逻辑被硬编码封装,没法替换或监控- 如果
DoSomething的逻辑依赖外部资源(比如数据库、文件)或修改全局状态,没有抽象层的话根本没法隔离依赖,验证结果全靠碰运气 - 你遇到的TestCase编译错误,大概率是DUnitX对
TMyRecord这种自定义记录类型的参数支持问题——要么是[TestCase]属性没法直接传记录,要么是测试方法的参数签名不匹配
二、重构建议(提升可测试性)
1. 让核心逻辑可直接访问
把DoSomething改成protected或者public(如果业务逻辑允许),或者提取接口,让核心逻辑脱离类方法的封装:
type IMyClassLogic = interface ['{GUID}'] procedure DoSomething(const AData: TMyRecord); end; TMyClass = class(TInterfacedObject, IMyClassLogic) public procedure DoSomething(const AData: TMyRecord); class procedure Execute(const AData: TMyRecord); end; implementation class procedure TMyClass.Execute(const AData: TMyRecord); var Logic: IMyClassLogic; begin Logic := TMyClass.Create; Logic.DoSomething(AData); end;
这样测试时可以直接创建实例调用DoSomething,不用绕Execute的实例化逻辑。
2. 隔离外部依赖
如果DoSomething依赖日志、数据库这类外部资源,必须把这些依赖抽象成接口,通过构造函数注入:
type ILogger = interface ['{GUID}'] procedure Log(const Msg: string); end; TMyClass = class(TInterfacedObject, IMyClassLogic) private FLogger: ILogger; public constructor Create(const ALogger: ILogger); procedure DoSomething(const AData: TMyRecord); class procedure Execute(const AData: TMyRecord; const ALogger: ILogger); overload; class procedure Execute(const AData: TMyRecord); overload; // 保留原有接口,内部用默认实现 end; implementation constructor TMyClass.Create(const ALogger: ILogger); begin inherited Create; FLogger := ALogger; end; class procedure TMyClass.Execute(const AData: TMyRecord; const ALogger: ILogger); var Logic: IMyClassLogic; begin Logic := TMyClass.Create(ALogger); Logic.DoSomething(AData); end; class procedure TMyClass.Execute(const AData: TMyRecord); begin Execute(AData, TDefaultLogger.Create); // 原有逻辑不变,兼容旧代码 end;
测试时传个Mock的ILogger,就能验证DoSomething是不是按预期调用了日志方法。
三、DUnitX测试用例编写
1. 解决TestCase编译错误
DUnitX的[TestCase]属性没法直接传自定义记录,改用[TestCaseSource]提供测试数据:
type TMyClassTest = class(TTestCase) private function GetTestData: TArray<TMyRecord>; public [TestCaseSource('GetTestData')] procedure TestDoSomething(const AData: TMyRecord); end; implementation function TMyClassTest.GetTestData: TArray<TMyRecord>; begin SetLength(Result, 2); Result[0].Value := 10; // 假设TMyRecord有Value字段 Result[1].Value := 20; end; procedure TMyClassTest.TestDoSomething(const AData: TMyRecord); var MyClass: TMyClass; MockLogger: TMock<ILogger>; begin // 初始化Mock依赖 MockLogger := TMock<ILogger>.Create; MyClass := TMyClass.Create(MockLogger); try // 执行测试方法 MyClass.DoSomething(AData); // 验证行为:检查MockLogger是否按预期被调用 MockLogger.Received(TMockMode.ExactlyOnce).Log('处理数据: ' + IntToStr(AData.Value)); finally MyClass.Free; MockLogger.Free; end; end;
2. 验证DoSomething的行为
- 状态验证:如果
DoSomething修改了传入的记录(注意记录是值类型,得是var参数才会影响外部),或者修改了某个可观察对象,直接断言结果是否符合预期 - 行为验证:依赖外部服务的话,用DUnitX自带的
TMock验证方法调用次数、参数是否正确 - 输出验证:如果逻辑有输出(比如写文件、返回值),直接检查输出内容是否符合预期
四、临时测试方案(不重构原有类)
如果暂时不想动原有代码,可以子类化TMyClass来暴露私有方法:
type TTestableMyClass = class(TMyClass) public procedure TestDoSomething(const AData: TMyRecord); end; procedure TTestableMyClass.TestDoSomething(const AData: TMyRecord); begin inherited DoSomething(AData); end;
测试时创建TTestableMyClass实例,调用TestDoSomething即可。
内容的提问来源于stack exchange,提问作者plumothy
相关产品推荐
相关产品推荐

