Files

171 lines
8.9 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: 测试
---
# 测试
这页讲清 IoT DC3 后端的测试怎么分层、什么时候必须补测试、用哪条命令跑哪一层。读完你能选对测试类型、在本地把单元/集成/E2E 跑起来,并知道改一处代码该补哪种测试才算"完成"。
> 你在这里:已读过[开发概览与规范](./)、想把改动验证到可合并。写驱动的测试要点另见[驱动开发](./driver-authoring)。
## 为什么要分层
测试不是越重越好。一个断言能在毫秒级捕获的逻辑错误,没必要拉起 PostgreSQL 容器去验;而跨服务的消息契约、时序库的落库行为,靠纯 Mock 又测不出真实问题。IoT DC3 据此把测试分成三层,越往下越快、越多、越孤立,越往上越慢、越少、越接近真实链路:
- **单元测试**最多,跑得最快,验证孤立的业务逻辑,不启动 Spring 上下文。
- **集成测试**用 Testcontainers 拉起真实 PostgreSQL/TimescaleDB、RabbitMQ、MQTT,验证 DAL、gRPC、消息这些跨组件协作。
- **E2E 测试**最少,验证端到端业务链路(命令下发、事件路由、时序表操作),默认关闭,靠环境变量显式启用。
选层的判断很直接:能不碰外部依赖就用单元测试;非要真容器才能复现的(SQL、消息、gRPC 契约)才上集成;只有验证整条链路时才动 E2E。
## 三层金字塔与对应命令
下图把三层、各自的范围与启动方式、以及触发命令对齐在一起。底层执行器(Surefire/Failsafe)和门禁(`DC3_E2E`)决定了同一次 `mvn` 会跑到哪一层。
```mermaid
flowchart TB
subgraph E2E["E2E · 最少 · 最慢"]
E["dc3-e2e<br/>端到端业务链路<br/>命令下发 / 事件路由 / 时序表"]
end
subgraph IT["集成 · 中量 · 中速"]
I["*IT.java(Failsafe 执行)<br/>DAL / gRPC InProcess / 消息契约<br/>真实 PostgreSQL / RabbitMQ / MQTT"]
end
subgraph UT["单元 · 最多 · 最快"]
U["*Test.java(Surefire 执行)<br/>孤立业务逻辑<br/>不启动 Spring 上下文"]
end
U -->|"make test"| I
I -->|"make test-it(需容器运行时)"| E
E -->|"make test-e2e(DC3_E2E=true + 起栈)"| Done["合并前:make coverage 聚合 JaCoCo"]
```
::: warning 集成与 E2E 需要容器运行时
`make test-it` 和 `make test-e2e` 都通过 Testcontainers 在运行时拉起 PostgreSQL/TimescaleDB、RabbitMQ 等容器,因此本地必须有可用的容器运行时(podman 或 Docker)。E2E 在此基础上还会在共享 Docker 网络上引导一整套真实依赖。没有容器运行时时,这两条命令会失败而非跳过——`make test`(纯单元)不受影响。
:::
各层的目标、执行器与技术栈对照如下(作参考,细节以上文为准):
| 层级 | 目标 | 执行器 / 门禁 | 技术方式 |
|------|------|---------------|----------|
| 单元测试 | 快速验证孤立业务逻辑 | Surefire | JUnit 5、Mockito 5、AssertJ;Reactor `StepVerifier` 验证响应式 |
| 集成测试 | 验证真实基础设施与跨模块协作 | Failsafe;`*IT.java` | Testcontainers、gRPC InProcess、RabbitMQ harness |
| E2E 测试 | 验证端到端业务链路 | `@EnabledIfEnvironmentVariable(named = "DC3_E2E")` | `dc3-e2e`、Testcontainers(共享 Docker 网络) |
## 本地怎么跑
后端命令统一走 `iot-dc3/` 下的 Makefile。最常用的四条:
```bash
make test # 单元测试(Surefire)
make test-it # 集成测试(Failsafe + Testcontainers,需容器运行时)
make test-e2e # E2E:等价于 DC3_E2E=true mvn -pl dc3-e2e -am -Pe2e verify
make coverage # 聚合 JaCoCo 报告(dc3-coverage -am verify)
```
`make test-e2e` 本身已设置 `DC3_E2E=true` 并只在 `dc3-e2e` 模块上跑 `-Pe2e`,无需手动导出环境变量。只想跑单个模块或单个用例时,直接用 Maven:
```bash
# 指定模块的单元测试
mvn -s .mvn/settings.xml test -pl dc3-common/dc3-common-manager
# 单个测试类 / 单个方法
mvn -s .mvn/settings.xml test -pl dc3-common/dc3-common-manager -Dtest=DeviceControllerTest
mvn -s .mvn/settings.xml test -pl dc3-common/dc3-common-public -Dtest="RTest#testOkWithData"
```
`make coverage` 完成后,聚合报告落在:
```text
dc3-coverage/target/site/jacoco-aggregate/index.html
```
::: info Failsafe 为何指向 outputDirectory
父 POM 给 Failsafe 配置了 `classesDirectory=${project.build.outputDirectory}`。原因是 Spring Boot 会把模块产物 repackage 成可执行 fat jar,那个 jar 在 Failsafe classpath 上不可直接加载;让集成测试针对未重打包的普通编译类运行,才能正确加载被测类。这是改动驱动等可执行模块的集成测试时需要留意的坑。
:::
## 前端测试命令
前端(`iot-dc3-web/`)用 pnpm + Vitest(单元/接口/组件/视图)与 Playwright(E2E),与后端独立:
::: code-group
```bash [Vitest 分套]
pnpm test # 全部 Vitest 套件
pnpm test:unit # tests/unit
pnpm test:api # tests/api
pnpm test:component # tests/component
pnpm test:views # tests/views
pnpm test:guard # tests/guardrails(AI 编码护栏)
pnpm test:ci # vitest run --coverage(CI 门禁)
```
```bash [Playwright E2E]
pnpm test:e2e # headless chromium
pnpm test:e2e:headed # 可见浏览器(E2E_HEADLESS=false)
```
:::
## 什么时候必须补测试
核心约定:**改 bug 先写能复现的回归测试,再修。** 把"这个 bug 不再出现"变成一条可执行、会失败的测试,修完它变绿,才算闭环。其余变更按风险补测:
| 变更类型 | 要求 |
|----------|------|
| Bug 修复 | 先补能复现问题的回归测试,再实现修复 |
| 新功能 / 行为变化 | 补单元测试,按风险追加集成测试 |
| 重构 | 保持现有覆盖;隐式契约补 contract 测试 |
| DAL / SQL 变更 | 补 Testcontainers 测试并运行 `make test-it` |
| gRPC proto 变更 | 同步更新服务端、客户端与契约测试 |
| 纯文档 / 格式变更 | 不需要 Java 测试,做文档构建或格式检查即可 |
## 可复用的测试基础设施
`dc3-common-test` 模块沉淀了跨模块共享的容器与基类,避免每个模块各搭一套。集成测试直接复用这些单例容器和 harness:
| 工具 | 用途 |
|------|------|
| `PgTimescaleContainer` | 单例 `timescale/timescaledb-ha:pg18` 容器,数据库与时序表测试 |
| `RabbitContainer` | RabbitMQ 容器,消息发布、确认、消费测试 |
| `MqttContainer` | MQTT 容器,MQTT 驱动测试 |
| `GrpcInProcessExtension` | JUnit 5 扩展:每个测试一组 in-process gRPC server + 托管 channel |
| `RabbitTestHarness` | 测试内收发 RabbitMQ,`awaitTrue()` 基于 Awaitility |
| `FixedClockConfig` | `@TestConfiguration`,把 `Clock` bean 固定到确定时刻 |
两个契约测试基类用于守住横切约定:
- `EnumContractTest<E>`:通过 `@TestFactory` 验证枚举 `getIndex()` 唯一、`ofIndex()` 往返、常量名稳定。
- `SecretFieldContractTest`:验证 `apiKey`、`password`、`secret`、`token` 等敏感字段不从 `@ToString` 与序列化中泄漏。
::: tip 时间、随机数与等待
注入 `java.time.Clock`,不要直接 `LocalDateTime.now()`;需要固定时间用 `FixedClockConfig`。异步等待用 Awaitility,禁止裸用 `Thread.sleep`。WebFlux 用 `WebTestClient`,不用 `MockMvc`;gRPC 用 in-process channel,不打开真实 socket。
:::
## 覆盖率门禁
`make coverage` 聚合各模块的 JaCoCo 数据,由 `dc3-coverage/pom.xml` 的门槛把关:
| 指标 | 当前门槛 |
|------|----------|
| 行覆盖率 | `coverage.line.minimum = 0.20`(20%) |
| 分支覆盖率 | `coverage.branch.minimum = 0.15`(15%) |
门槛当前相对克制,便于测试体系扩展期持续推进;判定只看静态最低阈值——任一指标低于上表门槛即阻断改动,不做与历史基线的回退比较。提高门槛时,应同时提交能支撑新门槛的测试,而非只调数字。
## CI 工作流
PR 与推送在 GitHub Actions 上分三个工作流跑测试,与本地命令一一对应:
| Workflow | 触发 | 主要任务 |
|----------|------|----------|
| `ci.yml` | push / PR 到 develop、release、main | 快速编译 |
| `test.yml` | push / PR 到 develop、release、main | 单元、集成、覆盖率 |
| `e2e.yml` | push 到 develop、release、main 或手动触发 | E2E |
合并前应确认:单元与集成 job 通过;覆盖率不低于 `dc3-coverage/pom.xml` 门槛(低于即阻断);行为变更在说明里写清已验证内容与未验证风险。
## 延伸阅读
- [开发概览与规范](./) — 二次开发的整体约定与命令入口
- [驱动开发](./driver-authoring) — 派生新驱动时如何补协议层与集成测试
- [环境变量](../quickstart/environment) — 本地 Java 运行所需的依赖主机与端口