
本文详解如何通过依赖注入(di)解耦服务层与数据访问逻辑,结合 mockito 实现对 `create()` 等核心业务方法的可测性设计,覆盖异常路径与正常流程,并避免滥用 spy 或对被测类自身打桩。
在单元测试实践中,一个常见但棘手的问题是:如何干净、可靠地测试那些依赖内部方法调用(如校验、查询、转换)的业务方法? 尤其当这些依赖逻辑又与外部资源(如内存数据库)强耦合时,测试往往陷入“无法控制输入”或“不得不打桩被测类自身”的困境——这不仅违背测试隔离原则,也暴露了代码设计上的可测性缺陷。
根本解法不在测试技巧,而在重构设计:将隐式依赖(如 MemoryDatabase.getInstance())显式化为可注入的抽象依赖。观察原始代码:
@Service
public class EntityService implements FilteringInterface {
private MemoryDatabase db = MemoryDatabase.getInstance(); // ❌ 静态单例 → 不可替换、不可模拟
...
}该写法导致 db 成为硬编码实现,测试时既无法预置特定数据集,也无法验证 db.add() 是否被调用。正确做法是引入接口抽象并依赖注入:
// 1. 定义持久化接口(替代具体 MemoryDatabase)
public interface Persistence {
Set getEntities();
void add(Entity entity);
}
// 2. 修改服务类:通过构造器注入,而非静态获取
@Service
public class EntityService implements FilteringInterface {
private final Persistence db; // ✅ final + 接口类型
public EntityService(Persistence db) { // ✅ 构造器注入
this.db = db;
}
public EntityDTO create(EntityDTO dto) throws Exception {
validateUniqueFields(dto);
Entity entity = Entity.toEntity(dto, "id1");
db.add(entity); // 可验证行为
return new EntityDTO.Builder(entity);
}
// ... 其余方法保持不变
} 如此重构后,测试即可完全掌控 Persistence 行为:
@ExtendWith(MockitoExtension.class)
class EntityServiceTest {
@Mock
private Persistence persistence;
@InjectMocks
private EntityService entityService;
@Test
void shouldThrowWhenNameAlreadyExists() {
// 给定:DB 中已存在同名 Entity
Entity existing = new Entity(1L, 1L, "conflict-name", "other-code");
when(persistence.getEntities()).thenReturn(Set.of(existing));
// 当:尝试创建同名 DTO
EntityDTO dto = new EntityDTO(null, "conflict-name", "new-code");
// 则:抛出异常
assertThrows(Exception.class, () -> entityService.create(dto));
}
@Test
void shouldPersistNewEntityOnSuccess() {
// 给定:空 DB
when(persistence.getEntities()).thenReturn(Set.of());
// 当:创建唯一 DTO
EntityDTO dto = new EntityDTO(null, "unique-name", "unique-code");
entityService.create(dto);
// 则:verify db.add() 被调用一次
verify(persistence).add(any(Entity.class));
}
}? 关键设计原则:依赖抽象,而非实现:Persistence 接口隔离了数据访问细节,使 EntityService 仅关注业务规则。构造器注入优先:确保依赖不可变且显式,便于测试时精准替换。FilteringInterface 无需 Mock:其默认方法是纯函数式逻辑(无状态、无副作用),可直接调用;测试重点应放在 validateUniqueFields() 的结果(是否抛异常),而非内部如何过滤——这由 filterEntityByNameOrCode() 的单元测试单独覆盖。避免 @Spy 和 @Mock 被测类:它们破坏测试边界,易导致“测试自己而非行为”。
此外,针对 FilteringInterface 中的默认方法,建议为其单独编写工具类测试(不依赖 Spring 上下文),例如:
@Test
void filterByNameOrCode_returnsMatchingEntities() {
Set list = Set.of(
new Entity(1L, 1L, "a", "x"),
new Entity(2L, 1L, "b", "y")
);
Set result = FilteringInterface.super.filterEntityByNameOrCode("a", "y", list);
assertEquals(2, result.size()); // 匹配 name="a" 或 code="y"
} 总结:可测性是良好设计的副产品。当你发现某个方法难以测试时,优先审视其依赖是否可替换、职责是否单一、边界是否清晰。通过 DI 解耦持久层、聚焦接口契约、分离纯逻辑与副作用,不仅能写出高覆盖率的测试,更能构建出更健壮、可维护的系统架构。










