如何正确测试依赖其他方法调用的服务类方法?
我查阅了大量资料,仍无法确定测试依赖其他方法调用结果的方法的最佳方案,参考过多个相关问题。目前我在实践TDD,开发一个无数据库的REST API服务,控制器调用EntityService的create方法处理DTO。
现有代码
EntityService类
@Service public class EntityService implements FilteringInterface { private MemoryDatabase db = MemoryDatabase.getInstance(); //Create method called from controller: receives DTO to create a new //Entity after validating that it's name and code parameters are unique public EntityDTO create(EntityDTO dto) throws Exception { validateUniqueFields(dto); Entity entity = Entity.toEntity(dto, "id1"); //maps DTO to Entity object db.add(entity); return new EntityDTO.Builder(entity);//maps entity to DTO } public void validateUniqueFields(EntityDTO dto) throws Exception { Set<Entity> foundEntities = filterEntityByNameOrCode(dto.getName(), dto.getCode(), db.getEntities()); if (!foundEntities.isEmpty()) { throw new Exception("Already exists"); } } }
FilteringInterface接口
public interface FilteringInterface { default Set<Entity> filterEntityByNameOrCode(String name, String code, Set<Entity> list) { return list.stream().filter(e -> e.getSiteId().equals(siteId) && (e.getName().equals(name) || e.getCode().equals(code))).collect(Collectors.toSet()); } default Optional<Entity> filterEntityById(String id, Set<Entity> list) { return list.stream().filter(e -> e.getId().equals(id)).findAny(); }; }
需要测试的场景
- DTO名称已存在时抛出异常;
- DTO编码已存在时抛出异常;
- DTO名称和编码均已存在时抛出异常;
- 名称和编码均唯一时,创建Entity并返回DTO。
核心疑问
测试这些场景时,无法通过Mockito mock内部调用的filterEntityByNameOrCode等方法(mock被测类不符合规范,Spy也不被推荐),不知该如何处理。另外,若当前代码设计导致测试困难,正确的代码设计方案是什么?服务类的正确测试方式又是什么?
一、不修改代码的测试解决方案
你不需要mock内部的filterEntityByNameOrCode方法,因为这个方法的行为完全依赖于MemoryDatabase中的数据。直接通过控制MemoryDatabase的测试数据来覆盖所有场景即可:
- 场景1(名称已存在):先往
db中添加一个名称与测试DTO相同、编码不同的Entity,调用create方法,验证是否抛出指定异常; - 场景2(编码已存在):先往
db中添加一个编码与测试DTO相同、名称不同的Entity,调用create方法,验证异常; - 场景3(名称和编码都存在):先往
db中添加一个与测试DTO名称和编码都一致的Entity,调用create方法,验证异常; - 场景4(唯一):确保
db为空,调用create方法后,验证返回的DTO字段正确,同时检查db中是否新增了对应的Entity。
注意:测试前要重置MemoryDatabase的状态,避免不同测试用例互相干扰(比如给MemoryDatabase加一个clear方法)。
二、代码设计优化方案(提升可测试性)
当前代码的测试痛点在于依赖了单例的MemoryDatabase和接口默认方法,耦合度较高。可以从以下两点重构:
1. 依赖注入MemoryDatabase
把单例的MemoryDatabase改为通过构造函数注入,这样测试时可以传入一个独立的测试实例,无需担心全局状态污染:
@Service public class EntityService implements FilteringInterface { private final MemoryDatabase db; // 构造函数注入 public EntityService(MemoryDatabase db) { this.db = db; } // 原有create、validateUniqueFields方法不变 }
测试时直接new一个干净的MemoryDatabase实例传入即可。
2. 拆分过滤逻辑为独立服务
把FilteringInterface中的默认方法抽成独立的EntityFilterService,让EntityService依赖这个服务,这样测试时可以灵活mock过滤逻辑(虽然大多数情况下还是推荐用真实实现+测试数据):
// 独立的过滤服务 @Service public class EntityFilterService { public Set<Entity> filterEntityByNameOrCode(String name, String code, Set<Entity> list) { return list.stream().filter(e -> e.getSiteId().equals(siteId) && (e.getName().equals(name) || e.getCode().equals(code))).collect(Collectors.toSet()); } public Optional<Entity> filterEntityById(String id, Set<Entity> list) { return list.stream().filter(e -> e.getId().equals(id)).findAny(); } }
修改EntityService:
@Service public class EntityService { private final MemoryDatabase db; private final EntityFilterService filterService; public EntityService(MemoryDatabase db, EntityFilterService filterService) { this.db = db; this.filterService = filterService; } public EntityDTO create(EntityDTO dto) throws Exception { validateUniqueFields(dto); Entity entity = Entity.toEntity(dto, "id1"); db.add(entity); return new EntityDTO.Builder(entity); } public void validateUniqueFields(EntityDTO dto) throws Exception { Set<Entity> foundEntities = filterService.filterEntityByNameOrCode(dto.getName(), dto.getCode(), db.getEntities()); if (!foundEntities.isEmpty()) { throw new Exception("Already exists"); } } }
三、Service类的正确测试方式
- 测试行为而非内部实现:关注输入对应的输出和副作用(比如数据是否被正确添加到db),不要纠结内部调用了哪些方法;
- 避免mock被测类:mock被测类会让测试和实现细节绑定,一旦代码重构,测试就会失效;
- 使用真实依赖或可控的测试依赖:对于无状态的依赖(比如
EntityFilterService),优先用真实实现;对于有状态的依赖(比如MemoryDatabase),用测试专用的实例,保证每个测试用例的独立性。
内容的提问来源于stack exchange,提问作者avaros

