You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

单元测试实践:如何可测试化动态实例化对象的Java代码?

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. 若追求严格单元测试:方案1是合理选择。@VisibleForTesting注解已经明确标识该方法是为测试暴露的,并非设计漏洞——像Guava这类成熟框架也常用这种方式平衡封装性和可测试性。只要createTaskRunner()的逻辑足够简单(仅负责创建实例),后续不会因业务变更变得复杂,就不会带来额外维护负担。

  2. 若侧重业务流程验证:方案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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 00:54:18