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:
+123
-1
@@ -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-Docker(dind),托管 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.yml,CI 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)
|
||||
|
||||
Reference in New Issue
Block a user