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

如何正确测试依赖其他方法调用的服务类方法?

问题:测试依赖内部方法的Service类的困境与优化方案

我查阅了大量资料,仍无法确定测试依赖其他方法调用结果的方法的最佳方案,参考过多个相关问题。目前我在实践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

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.08.05 14:55:20