feat: Enhance documentation on layering, object naming conventions, and API design

- Added object naming conventions (PO/DAO/BO/DTO/VO) in 02-layering.md to clarify terminology and usage within the team.
- Updated 06-api-design.md to include MapStruct for DTO and entity conversion, providing examples and configuration details.
- Expanded 07-config-governance.md with local development instructions and strategies for running without K8s, including two recommended approaches.
- Included K8s probe configuration details in 08-observability.md for liveness and readiness checks.
- Clarified CI/CD processes in 09-build-deploy.md, detailing environment distinctions and deployment strategies for local, Dev, UAT, and Prod.
- Introduced ArchUnit for architectural testing in 10-testing.md, ensuring adherence to defined layering rules and coverage verification with Jacoco.
This commit is contained in:
Guangfei.Zhao
2026-08-13 15:19:05 +08:00
parent 1e0cbb86a2
commit 8c0fcd84e8
6 changed files with 518 additions and 30 deletions
+123 -1
View File
@@ -132,6 +132,127 @@ class F6ApiClientResilienceTest {
}
```
## `ArchUnit`:把分层规则变成可执行的测试
[01-project-structure.md](./01-project-structure.md) 和 [02-layering.md](./02-layering.md) 里定的依赖方向规则(`domain` 不依赖 Spring/JPA、domains 之间不互相依赖、`api` 不直接依赖 `infrastructure`),光靠 code review 肉眼盯着容易漏,模块一多更盯不过来。用 [ArchUnit](https://www.archunit.org/) 把这些规则写成测试,每次构建自动检查:
```groovy
// build.gradle(专门放架构测试的模块,或加进 bootstrap 的 test 依赖)
testImplementation 'com.tngtech.archunit:archunit-junit5:1.3.0'
```
```kotlin
// architecture-test/src/test/kotlin/.../LayeringRulesTest.kt
class LayeringRulesTest {
private val classes = ClassFileImporter().importPackages("com.continental.retailapp")
@Test
fun `domain 层不能依赖 Spring 或 JPA`() {
noClasses()
.that().resideInAPackage("..domain..")
.should().dependOnClassesThat().resideInAnyPackage("org.springframework..", "jakarta.persistence..")
.check(classes)
}
@Test
fun `domains 之间不能互相依赖(bff-orchestration 除外)`() {
slices()
.matching("com.continental.retailapp.(*)..")
.should().notDependOnEachOther()
.ignoreDependency(
DescribedPredicate.describe("来自 bff-orchestration") { it.resideInAPackage("..bffOrchestration..") },
DescribedPredicate.alwaysTrue(),
) // bff-orchestration 允许依赖多个 domain,是唯一的例外,见 02-layering.md
.check(classes)
}
@Test
fun `api 层不能直接依赖 infrastructure 层`() {
noClasses()
.that().resideInAPackage("..api..")
.should().dependOnClassesThat().resideInAPackage("..infrastructure..")
.check(classes)
}
}
```
这类测试一次写好,覆盖的是全代码库范围的结构性规则,跑在 CI Validation 阶段(见 [09-build-deploy.md](./09-build-deploy.md)),比"等 code review 时人工发现某个 domain 偷偷 import 了另一个 domain 的 entity"要可靠得多,而且不区分改动大小——哪怕只加了一行 `import`,只要违反规则就会立刻挂红。
## CI 里跑 Testcontainers 的前提条件
`infrastructure` 层的集成测试(前面 `StoreJpaRepositoryTest` 那个例子)依赖 Testcontainers 起真实容器,这要求执行 `./gradlew test` 的 GitLab Runner 本身**能起 Docker 容器**,不是随便一个 Runner 都能跑,两种常见配置:
**方式一:Docker-in-Dockerdind),托管 Runner 的默认选择**
```yaml
# .gitlab-ci.yml
unit-integration-test:
stage: validate
image: eclipse-temurin:21-jdk
services:
- docker:24-dind
variables:
DOCKER_HOST: tcp://docker:2375
DOCKER_TLS_CERTDIR: ""
script:
- ./gradlew test --no-daemon
```
**方式二:挂载宿主机 Docker socket,适合 [09-build-deploy.md](./09-build-deploy.md) 里那种自建 self-hosted Runner`azure-vnet-runner`**
```toml
# GitLab Runner 的 config.toml
[[runners]]
[runners.docker]
privileged = true
volumes = ["/var/run/docker.sock:/var/run/docker.sock", "/cache"]
```
方式二没有 dind 的嵌套虚拟化开销,跑起来更快,但要求 Runner 能访问宿主机的 Docker socket(等价于 Runner 对宿主机有较高权限),只适合放在我们自己管控的 self-hosted Runner 上;如果以后接入公共共享 Runner 跑这一类测试,只能用方式一,不应该在共享 Runner 上开 Docker socket 权限。
## 覆盖率门禁(Jacoco
```groovy
// build.gradle
plugins {
id 'jacoco'
}
jacocoTestReport {
dependsOn test
reports {
xml.required = true // CI 里给 GitLab 覆盖率可视化用
}
}
jacocoTestCoverageVerification {
violationRules {
rule {
limit {
minimum = 0.70
}
}
}
}
check.dependsOn jacocoTestCoverageVerification // 覆盖率不达标,./gradlew check 直接失败
```
```yaml
# .gitlab-ci.ymlCI Validation 阶段追加覆盖率门禁
unit-integration-test:
stage: validate
script:
- ./gradlew test jacocoTestCoverageVerification --no-daemon
artifacts:
reports:
coverage_report:
coverage_format: cobertura
path: '**/build/reports/jacoco/test/jacocoTestReport.xml'
```
阈值定在 70% 而不是追求 90%+:`domain` 层因为纯逻辑、mock 成本低,覆盖率天然会很高;`infrastructure`/`api` 层的诉求是"关键路径别漏测"而不是"每一行都要覆盖",统一定一个较低的整体阈值,作用是**拦住完全没写测试就合入的代码**,而不是逼着每个模块都卷到很高的数字——那样容易导致为了凑覆盖率写没有意义的测试。
## 附录:为什么 domain 层用 mock、infrastructure 层坚持用真实依赖
这是测试金字塔的实际落地取舍:越往下层(domain)测试数量应该越多、跑得越快,因为业务规则的分支组合往往很多(各种边界条件),用 mock 把依赖都隔离掉才能便宜地把每个分支都测到;越往上/往基础设施层,测试数量应该越少但真实度要求越高,因为这一层要验证的恰恰是"我们对某个具体技术(JPA、真实数据库、真实 HTTP 依赖)的假设是否成立"——如果这一层也用 mock,等于假设了"这个假设是对的",那测试就失去了意义。
@@ -140,7 +261,6 @@ Testcontainers 和 WireMock 的共同点是:它们让"跑得慢、需要真实
## 待补充
- 各 domain 覆盖率要求。
- 是否需要和 APP 端做端到端契约测试(比如引入 Pact)。
## 参考链接
@@ -149,3 +269,5 @@ Testcontainers 和 WireMock 的共同点是:它们让"跑得慢、需要真实
- [Testcontainers 官方文档](https://testcontainers.com/)
- [WireMock 官方文档](https://wiremock.org/docs/)
- [Martin Fowler: Test Pyramid](https://martinfowler.com/bliki/TestPyramid.html)
- [ArchUnit 官方文档](https://www.archunit.org/userguide/html/000_Index.html)
- [Jacoco Gradle Plugin 官方文档](https://docs.gradle.org/current/userguide/jacoco_plugin.html)