docs: 빌드·CI 감사 계획과 진행 중이던 문서 정리
- CI 단계 분리 계획 추가 (docs/superpowers/plans/2026-09-16-ci-stage-separation.md). 빌드·CI 레이어 전수 리뷰 133건의 결론과 Track A/B/C 작업 순서를 담는다. - public-path 보안 기준선을 실제 배포 기본값(/v1/healthcheck)으로 재생성. 이전 값은 gitignore 된 src/.env 에서 유래해 재현이 불가능했다. - 진행 중이던 ADR·리뷰·테스트 전략 문서 반영, 대체된 grpc 계획 문서 제거. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5
parent
21234e38cd
commit
2a8d34f85c
@@ -0,0 +1,119 @@
|
||||
# ADR-BUILD-001: `java-test-fixtures` is the standard for shared test code
|
||||
|
||||
- Status: Accepted
|
||||
- Date: 2026-09-07
|
||||
- Scope: every leaf that publishes or consumes shared test code
|
||||
- Source: `docs/reviews/2026-09-07-app-bootstrap-module-code-review.md` BOOT-015
|
||||
|
||||
## Context
|
||||
|
||||
Two conventions do the same job in this repository.
|
||||
|
||||
`ca.testkit-publisher` — a convention plugin — gives a leaf a `testkit` source set, wires its output
|
||||
onto the lanes that leaf names, and optionally publishes it as a consumable configuration. Five
|
||||
leaves use it: `persistence-jpa` (published as `jpaTestkit`), `web` (`webTestkit`), `websocket`
|
||||
(`websocketTestkit`), `persistence-mongo` and `httpclient` (both unpublished).
|
||||
|
||||
`java-test-fixtures` — Gradle's own plugin — gives a leaf a `testFixtures` source set, puts it on
|
||||
`test`'s classpath automatically, and always publishes it as a variant consumers reach with
|
||||
`testFixtures(project(':x'))`. One leaf uses it: `graphql`, which additionally fails its build when a
|
||||
fixture is written outside `src/testFixtures/java`.
|
||||
|
||||
Two conventions for one purpose is the defect. A contributor adding shared test code has to know
|
||||
which leaf they are in before they know where the file goes, and the two answers are not
|
||||
interchangeable: a consumer of the first writes `project(path: ':x', configuration: 'jpaTestkit')`
|
||||
and has to know the configuration's name, while a consumer of the second writes
|
||||
`testFixtures(project(':x'))` and does not.
|
||||
|
||||
## Decision
|
||||
|
||||
**`java-test-fixtures` is the standard.** New shared test code goes in `src/testFixtures/java`, and a
|
||||
consumer depends on it with `testFixtures(project(':x'))`.
|
||||
|
||||
Three reasons, in order of weight:
|
||||
|
||||
1. **The consumer side describes itself.** `testFixtures(project(':adapter:inbound:web'))` says what
|
||||
it is. `project(path: ':adapter:inbound:web', configuration: 'webTestkit')` says where to look,
|
||||
and only after the reader has learned that `webTestkit` is a testkit rather than a lane.
|
||||
2. **The enforcement already exists and is copyable.** `graphql`'s build fails when a fixture is
|
||||
declared in the wrong place. The same guard applies unchanged to any leaf that adopts the plugin.
|
||||
3. **It is one fewer local concept.** A convention plugin that reimplements a Gradle plugin has to be
|
||||
maintained against it.
|
||||
|
||||
## What the local plugin does better, and how it is replaced
|
||||
|
||||
This is worth writing down, because the review that prompted this ADR recommended the migration
|
||||
before reading `ca.testkit-publisher`, and the plugin turns out to encode two deliberate decisions
|
||||
rather than being an oversight.
|
||||
|
||||
**Publishing is opt-in.** `persistence-mongo` and `httpclient` have a testkit and publish nothing;
|
||||
`persistence-jpa` publishes. The plugin's own comment names this as "a real difference in what each
|
||||
leaf offers rather than an oversight to normalise away". `java-test-fixtures` always creates the
|
||||
variant, so the distinction is lost — a leaf that never meant to offer its fixtures will offer them.
|
||||
|
||||
> Replacement: none at the build level. The distinction moves to review: the fixtures of a leaf that
|
||||
> nobody consumes are simply unconsumed. This is a real, accepted loss.
|
||||
|
||||
**Lane consumption is declared.** `persistence-jpa` says `consumedBy 'test', 'postgresqlIntegrationTest'`.
|
||||
`java-test-fixtures` puts fixtures on `test` only, so every other lane needs the output added
|
||||
explicitly.
|
||||
|
||||
> Replacement: `strictTestLanes`' existing `compilesAgainst` expresses this unchanged — a lane
|
||||
> declares `compilesAgainst 'main', 'testFixtures'`. The first draft of this ADR assumed the DSL
|
||||
> would need a change, because `sourceSet(name)` creates what it is given and `testFixtures` already
|
||||
> exists. The `persistence-mongo` migration showed otherwise: `compilesAgainst` only *looks a source
|
||||
> set up*, so naming a plugin-created one works as-is. What the leaf drops is the
|
||||
> `sourceSet('testkit')` declaration, not the lane's.
|
||||
|
||||
## Migration: done, and what it cost
|
||||
|
||||
Five leaves, eleven lanes, two published testkits, all migrated leaf by leaf with the suite run
|
||||
between each. `ca.testkit-publisher` is deleted.
|
||||
|
||||
The order was chosen so a mistake would be cheap: unpublished leaves first, published ones last with
|
||||
their consumer in the same step.
|
||||
|
||||
1. `persistence-mongo` — one leaf, two lanes, no cross-module consumer; the proof the path works.
|
||||
What it took, per leaf:
|
||||
- `apply plugin: 'java-test-fixtures'` at the top of the leaf build file;
|
||||
- `git mv src/testkit src/testFixtures`;
|
||||
- drop `sourceSet('testkit')` and the whole `testkitPublisher` block; keep every other lane's
|
||||
`compilesAgainst`, renaming `'testkit'` to `'testFixtures'`;
|
||||
- rename `testkitImplementation` to `testFixturesImplementation`, **and add what the old source
|
||||
set was inheriting silently**. This is the one non-mechanical step: `testkit*` extended
|
||||
`testImplementation`, so the fixtures saw every test library the leaf declared. Mongo's needed
|
||||
four more lines (AssertJ, BSON, Spring Data commons, Toxiproxy) — none of which the leaf had
|
||||
ever stated the fixtures depended on;
|
||||
- regenerate the leaf's lock state.
|
||||
2. `httpclient`, then `websocket` — unpublished as well, more lanes.
|
||||
3. `web` and `persistence-jpa` with `app-bootstrap`'s two consumer declarations, which became
|
||||
`testImplementation(testFixtures(project(':…')))`.
|
||||
4. `ca.testkit-publisher` deleted, along with its `plugins {}` entry and its application in the root
|
||||
build.
|
||||
|
||||
### Two things the migration broke, and what they taught
|
||||
|
||||
Both were caught by tests that exist to catch exactly this, which is the argument for having them.
|
||||
|
||||
**ArchUnit corpora went wrong in opposite directions.** `httpclient`'s boundary rules *excluded*
|
||||
`build/classes/java/testkit`; after the move the fixtures arrived as a `…-test-fixtures.jar` on the
|
||||
same classpath, so the exclusion missed them and 258 fixture-to-fixture calls were reported as
|
||||
production depending on the testkit. `persistence-jpa`'s rules *included* only
|
||||
`build/classes/java/main`; applying `java-test-fixtures` makes the module's own test classpath carry
|
||||
the module as a **jar** rather than as a class directory, so its corpus became empty. The second is
|
||||
the dangerous one — an empty corpus makes every `noClasses()` rule pass — and it surfaced only
|
||||
because that suite asserts its corpus is non-empty before asserting anything about it.
|
||||
|
||||
**Fixtures had invisible dependencies.** `testkit*` configurations extended `testImplementation`, so
|
||||
the fixtures compiled against every test library their leaf declared without ever naming one. Making
|
||||
them explicit took roughly thirty `testFixturesImplementation` lines across the five leaves —
|
||||
Micrometer, Spring Web, Netty, logback, Jackson, JUnit, AssertJ, Spring Data. None of them were
|
||||
wrong; none of them were stated.
|
||||
|
||||
## Consequences
|
||||
|
||||
- `docs/testing/TESTING_STRATEGY.md` §5 records the standard; this ADR records why and at what cost.
|
||||
- Until step 5, two conventions remain visible. The strategy document says so explicitly, so a
|
||||
contributor reading it is not left to infer which one is current.
|
||||
- The opt-in-publishing distinction is given up. If it later proves load-bearing — a leaf whose
|
||||
fixtures genuinely must not be reachable — the answer is a separate module, not a third convention.
|
||||
Reference in New Issue
Block a user