单元测试实践:如何可测试化动态实例化对象的Java代码?
原始代码
@AllArgsConstructor public class Foo { private final Dao dao; public void processRequest(String requestId) { if (this.isRequestNotFound(requestId)) { final TaskRunner runner = TaskRunner.builder().withDao(this.dao).build(); runner.execute(); } } private boolean isRequestNotFound(String requestId) { return this.dao.get(requestId) == null; } }
我希望将这段代码改造为可单元测试的版本,目前有两种备选方案:
方案1:封装包可见方法并使用Mockito Spy
把TaskRunner.builder().withDao(this.dao).build();封装为包可见方法,通过Mockito对Foo类进行spy来实现测试。
改造后的代码:
@AllArgsConstructor public class Foo { private final Dao dao; public void processRequest(String requestId) { if (this.isRequestNotFound(requestId)) { final TaskRunner runner = this.createTaskRunner(); runner.execute(); } } @VisibleForTesting TaskRunner createTaskRunner() { return TaskRunner.builder().withDao(this.dao).build(); } private boolean isRequestNotFound(String requestId) { return this.dao.get(requestId) == null; } }
优点:可以直接针对Foo类做单元测试,mock createTaskRunner()方法返回模拟的TaskRunner,验证execute()是否被正确调用。
缺点:createTaskRunner()原本是私有方法,仅Foo内部使用,改为包可见是否属于设计缺陷?
方案2:通过验证Dao的间接调用实现测试
由于TaskRunner依赖Dao且会调用Dao的API(例如dao.put(someData)),可以通过验证dao.put(any())是否被调用,间接确认runner.execute()的执行。
优点:不需要创建或暴露createTaskRunner()这类私有方法,保持代码封装性。
缺点:测试会同时覆盖TaskRunner和Foo的逻辑,更接近集成测试而非严格的单元测试。
困惑与思考
我纠结于哪种方案更符合可维护、可测试代码的最佳实践。我明白问题根源在于TaskRunner的初始化逻辑和业务逻辑(runner.execute())没有分离,但直接依赖注入TaskRunner又显得过度设计——毕竟当isRequestNotFound(...)为false时,根本不需要创建TaskRunner对象。
个人建议
两种方案没有绝对的优劣,核心取决于你对测试边界的定义和项目长期维护需求:
若追求严格单元测试:方案1是合理选择。
@VisibleForTesting注解已经明确标识该方法是为测试暴露的,并非设计漏洞——像Guava这类成熟框架也常用这种方式平衡封装性和可测试性。只要createTaskRunner()的逻辑足够简单(仅负责创建实例),后续不会因业务变更变得复杂,就不会带来额外维护负担。若侧重业务流程验证:方案2可以接受,但要注意潜在风险。如果后续TaskRunner的逻辑发生变化(比如不再调用
dao.put(),转而调用其他服务),测试会直接失效,需要同步修改测试代码,维护成本会随TaskRunner复杂度上升而增加。
另外,你担心的“依赖注入TaskRunner是否过度设计”可以换个思路:注入TaskRunner工厂类而非TaskRunner实例,这样既分离了对象创建与业务逻辑,又能按需创建实例:
@AllArgsConstructor public class Foo { private final Dao dao; private final TaskRunnerFactory runnerFactory; public void processRequest(String requestId) { if (this.isRequestNotFound(requestId)) { final TaskRunner runner = runnerFactory.create(dao); runner.execute(); } } private boolean isRequestNotFound(String requestId) { return this.dao.get(requestId) == null; } }
这种方式既符合依赖倒置原则,又能轻松mock工厂类返回模拟的TaskRunner,完全不需要暴露私有方法。工厂类本身逻辑简单,后续若TaskRunner的创建逻辑变化(比如需要新增参数),只需修改工厂类即可,Foo的代码不受影响,算不上过度设计。
内容的提问来源于stack exchange,提问作者noobie2023

