← 返回 Java 后端知识路线
阶段 05工程化与部署

JUnit 5、Mockito、集成测试与日志:让“能跑”变成证据

从一个文章发布用例建立单元测试、Mockito 协作验证、真实数据库集成测试和 Actuator 观测边界。

第 33 / 35 篇
JUnit 5Mockito集成测试日志Actuator

先看这一课值不值得学

学完后,你手里多了哪些代码积木

用测试、日志和健康端点证明服务真的可运行。

本课正式新增

语法 / API / 命令你必须会到什么程度
@Test / @ParameterizedTest声明普通或参数化测试
@Mock / when / verify隔离依赖并验证协作
@SpringBootTest启动 Spring 上下文做集成测试
Logger输出结构化、分级日志
Actuator endpoint暴露健康和运行指标

本课只借用,先别硬背

  • 完整监控平台不在本课搭建

学完必须能独立写

  • 为 Service 写单元和集成测试
  • 从失败日志和健康端点定位启动问题
本课目录
  1. 1. 现实问题:测试绿了,线上仍然发布了坏文章
  2. 2. 最小可运行示例:单测验证结果和协作
  3. 3. 调用链与对象变化
  4. 4. 为什么这样设计
  5. 5. 项目落点:为博客系统建立测试金字塔
  6. 6. 易错排查
  7. 7. 一页复习

1. 现实问题:测试绿了,线上仍然发布了坏文章

只测 Service 的 if 分支,可能漏掉 MyBatis SQL、数据库唯一约束、JSON 字段和事务代理;只做端到端测试,又会因为环境复杂而难以定位。测试不是数量竞赛,而是为调用链的不同边界提供证据。单元测试快,协作测试验证调用协议,集成测试验证真实组件组合,日志和 Actuator 证明运行时状态。

以“发布草稿”为例:Service 单测不启动 Spring,Mockito fake Repository;集成测试启动容器和数据库,验证状态、审计和回滚;接口测试验证 HTTP 202/409 与错误 JSON。

2. 最小可运行示例:单测验证结果和协作

@ExtendWith(MockitoExtension.class)
class PublishServiceTest {
    @Mock ArticleRepository repository;
    @Mock Clock clock;
    @InjectMocks PublishService service;

    @Test
    void publishesDraftAndWritesAudit() {
        Article draft = new Article(7L, "DRAFT");
        when(repository.findById(7L)).thenReturn(Optional.of(draft));

        service.publish(7L, "luna");

        verify(repository).save(argThat(article -> article.status().equals("PUBLISHED")));
        verify(repository).insertAudit(7L, "luna");
    }
}

这是语义示例,完整项目还需导入 JUnit 5 与 Mockito 依赖并根据领域对象调整构造器。verify 验证协作者协议,不能替代对最终数据库状态的集成测试;若单测只 verify 方法调用,却没有断言关键结果,测试很容易和实现细节绑死。

3. 调用链与对象变化

JUnit Platform 发现带 @Test 的方法,Extension 创建 mock 和被测 Service;Mockito 记录 stubbing,Service 读取 fake Article,改变一个测试对象或生成新对象,再调用 mock save/audit。verify 从调用记录中检查参数,测试结束后 extension 清理 mock。

集成测试则启动 ApplicationContext,真实 Bean 经过 IoC、AOP、事务和 Mapper,HTTP 请求可能经过完整 SecurityFilterChain,数据库返回真实行。日志把 request id、handler、SQL 模板耗时和结果状态串起来;Actuator health/metrics 从运行组件读取状态,提供与业务断言不同的观察面。

4. 为什么这样设计

单测要快且失败定位明确,所以替换数据库、时间和网络;集成测试要少而关键,覆盖事务、映射、配置和真实协议;契约/接口测试锁定外部 JSON。Mockito 不应 mock 自己拥有的值对象,也不应 mock 每个内部方法,否则测试只证明“调用了预期的调用”。

日志是结构化事件,不是把变量全部 println;记录 traceId、业务 id、结果、耗时和异常 cause,敏感字段脱敏。Actuator 的健康端点只暴露必要指标,readiness 反映是否可接流量,liveness 反映进程是否需要重启,二者不能混用。

5. 项目落点:为博客系统建立测试金字塔

每个发布规则有 Service 单测;关键 Mapper 与 MySQL 约束有集成测;Controller 有 MockMvc JSON 测;部署前跑一条真实创建—发布—读取流程。测试夹具使用独立数据库 schema、事务回滚或容器,每次运行记录 JDK、依赖、测试数和失败输出。

练习:增加重复发布、文章不存在、审计失败和事务回滚测试;用 Testcontainers 启动 MySQL,断言失败后 article 状态仍是 DRAFT;启动 Boot 应用访问 /actuator/health,确认数据库不可用时 readiness 反映真实状态。不要用 sleep 等待异步,使用 Awaitility 或明确的轮询上限。

6. 易错排查

  • 单测绿但 SQL 错:补一条真实数据库集成测试,不要把 SQL 全 mock 掉。
  • Mockito verify 过于脆弱:验证业务协作和关键参数,不锁死无关调用顺序。
  • 集成测试依赖开发库:使用临时容器/schema 和固定夹具,禁止读写个人数据。
  • 日志没有 traceId:检查入口过滤器、异步任务上下文和日志 pattern/JSON encoder。
  • Actuator 公开敏感端点:只暴露 health/info 等必要端点,并纳入 Security。

7. 一页复习

单元测验证纯业务,Mockito 验证协作协议,集成测验证真实组件,接口测验证 HTTP 契约;日志和 Actuator 提供运行时观察。每个测试都应回答“哪个边界被证明、失败如何定位”。绿灯只有在测试范围和环境证据清楚时才有意义。

发布用例的测试输入应有明确对象状态:DRAFT 文章、当前作者和固定 Clock;Service 单测输出是 PUBLISHED 领域对象和两次协作者调用;数据库集成测输出是 article 状态、audit 行和事务提交结果;MockMvc 输出是 HTTP 状态与 JSON。若只验证 verify(save),不能证明 SQL 条件、唯一键或回滚真的正确。

项目测试文件可以按边界放在 application/article/PublishServiceTestpersistence/article/ArticleMapperITweb/article/ArticleControllerTestsmoke/BlogFlowIT。单元测试替换 Repository、Clock 和外部客户端,集成测试使用独立 MySQL/容器,smoke 测试启动完整应用;每类测试都写清速度、依赖和失败定位,不把所有测试都标成 integration。

日志和 Actuator 的对象也要区分:日志事件包含 traceId、articleId、handler、elapsed 和 result,health 返回依赖状态,metrics 返回计数/延迟。健康端点 200 只代表配置的健康组认为可用,不能推出发布逻辑成功。异步任务要传递 traceId,测试日志要保留失败堆栈和数据库容器日志。

排错时单测失败查输入夹具和 stub,集成测失败查迁移/数据库/事务,接口测失败查 JSON/Filter/认证,线上失败查日志和 health/metrics。测试偶发失败通常来自共享静态状态、当前时间、线程竞态或外部服务;固定 Clock、隔离容器、CountDownLatch 和 fake transport,不能只增加重试次数。练习是让 audit 插入故意抛错,断言数据库状态与日志结果一致。

测试的输入输出要覆盖失败状态:DRAFT + 合法作者输出 PUBLISHED 和 audit,重复发布输出冲突且数据库不变,audit 写入异常输出 rollback,数据库断连输出基础设施错误。Mockito 只提供协作者的可控返回,真实数据库测试才知道条件 update、唯一键和事务是否有效。测试名称应写业务结果,不要只写方法名。

项目里可以把 Clock、UUID、文件存储和上游客户端都作为端口注入,单测固定时间和 id;集成测使用随机 schema、迁移脚本和真实连接池;接口测用 MockMvc 检查字段错误、Security 状态和 content type;smoke 测试从创建到读取走一遍。测试夹具的生命周期要在 afterEach/容器结束时清理,禁止依赖个人开发库。

日志与 Actuator 是另一种输出契约:日志适合追踪一次请求的路径和异常,metrics 适合看聚合趋势,health 适合判断是否能接流量。一个请求 200 不代表数据库、Redis、文件和消息都健康;一个 health 200 也不代表领域规则正确。观测字段、端点暴露和 Security 保护要一起审查。

排错时单测失败先看 stub 是否与输入一致,集成失败先看迁移和环境,接口失败先看真实 HTTP,偶发失败查时间、随机数、线程和共享状态;日志缺 traceId 查过滤器/异步上下文,指标缺数据查 actuator 暴露和 registry。练习是写一条发布成功、rollback 和健康失败的完整证据链,报告测试数、退出码、响应和数据库行。

验证清单:为发布用例准备成功、重复发布、审计失败、数据库断连和权限拒绝五组夹具,单测只替换端口并断言领域状态,Mapper 集成测检查真实 SQL/唯一键/事务,MockMvc 检查状态码和错误 JSON,smoke 测从创建到公开读取走完整应用。固定 Clock、UUID、traceId 和测试 schema,记录每类测试的数量、退出码、耗时与失败日志;如果只验证 Mockito 的 save 被调用,不能声称事务或条件更新正确。

观测复盘要把日志、metrics、health 分开:一次请求日志回答“走了哪条链”,指标回答“最近是否变慢/变多”,health 回答“是否满足接流量条件”。故意让 Redis、数据库和上游分别超时,确认日志包含 traceId/资源 id/安全错误摘要,Actuator 端点没有暴露秘密,异步任务也能关联原请求。测试完成后清理容器、临时文件和线程池,避免下一次绿灯依赖上一次残留状态。

进阶附录:测试隔离、并发与故障注入

并发用例要控制起点和等待,不要依赖线程调度偶然触发;故障注入可以模拟数据库断连、Redis 超时和上游 500,验证降级与重试上限。测试日志保留失败上下文,避免为了干净输出而吞掉异常堆栈。

本课按「JUnit 5、Mockito、Spring Boot Test、日志与 Actuator」的学习范围组织,正文与示例均为本站原创整理。