1. 测试工程导论
在Java项目里,“测试”经常被当成一个笼统的动作:写完代码跑一下、提交前点一遍、上线前让测试同学验一轮。但真正决定工程质量与迭代速度的,并不是“有没有测试”,而是测试的粒度、边界与职责是否清晰。本章聚焦两个最基础也最容易被混淆的概念:单元测试(Unit Test)与集成测试(Integration Test),并说明它们在Java工程中的价值与典型误区。
一、定义:测什么、由谁协作、隔离到什么程度
单元测试针对软件中最小的可测试单元进行验证。在Java中,这个“单元”通常是一个类的方法,或一组高度内聚、共同完成单一职责的方法。单元测试的核心特征是隔离:被测单元之外的依赖(数据库、HTTP服务、消息队列、文件系统、时间、随机数等)都应被替换为可控的测试替身(Stub、Fake、Mock),从而让测试只回答一个问题——“这段逻辑本身对不对”。
集成测试验证多个单元、模块或外部系统组合在一起时能否正确协作。它关心的不是某个方法内部的if-else,而是边界处的契约:Service调用Repository时SQL是否正确、事务是否生效、JSON序列化字段是否匹配、连接池配置是否可用、消息是否按预期投递与消费。集成测试允许甚至要求真实依赖参与,例如真实的H2/Testcontainers数据库、真实的Spring上下文、真实的HTTP客户端。
可以用一句话区分:单元测试验证“逻辑正确”,集成测试验证“连接正确”。
二、边界:一条可操作的划分线
边界不清是测试体系混乱的根源。判断一个测试属于哪一类,可以看三个问题:
- 是否启动Spring容器? 使用
@SpringBootTest通常意味着集成测试;纯JUnit + Mockito的POJO测试通常是单元测试。 - 是否跨越进程或I/O边界? 访问数据库、发HTTP请求、读写文件、发消息,都属于集成范畴。
- 失败时能否在秒级定位到单个类? 单元测试失败应直接指向某个方法;集成测试失败往往需要排查配置、网络、SQL、序列化等多个环节。
需要强调的是,边界不是靠“测试类放在哪个包”决定的,而是由运行时真实参与的依赖范围决定。一个类叫UserServiceTest,如果它启动了完整Spring上下文并连了数据库,它就是集成测试。
三、在Java项目中的价值
单元测试与集成测试不是“二选一”,而是测试金字塔中不同层级的协作。单元测试数量最多、执行最快,负责在开发阶段快速反馈逻辑缺陷;集成测试数量较少、执行较慢,负责在提交或流水线阶段拦截协作缺陷。二者共同支撑三件事:
- 重构安全网: 有了单元测试,修改一个方法的内部实现后,几秒内就能知道是否破坏了原有行为。
- 缺陷左移: 集成测试把“数据库字段类型不匹配”“事务未生效”这类问题从联调阶段提前到本地或CI阶段。
- 可执行的文档: 测试用例本身就是对接口契约和业务规则最精确的描述,比注释更不容易过期。
四、典型误区
误区一:把所有测试都写成@SpringBootTest。 这会让每个测试都启动完整容器,执行时间从毫秒级膨胀到秒级甚至分钟级,开发者逐渐不愿运行,测试形同虚设。正确做法是:纯逻辑用JUnit + Mockito,只有需要验证容器装配、SQL、序列化时才启动Spring。
误区二:单元测试里连真实数据库。 这会让单元测试变得脆弱且缓慢,还会因为数据状态互相污染导致随机失败。单元测试应通过Repository接口的Mock来隔离持久层。
误区三:追求100%覆盖率。 覆盖率是观察指标,不是目标。getter/setter、简单委托方法的覆盖对缺陷发现贡献极低。应优先覆盖分支逻辑、边界条件、异常路径和业务规则。
误区四:集成测试没有数据隔离。 多个测试共用同一个数据库且不清理数据,会导致执行顺序不同结果不同。应使用@Transactional回滚、@Sql初始化或Testcontainers每次重建 schema。