74 lines
5.6 KiB
Markdown
74 lines
5.6 KiB
Markdown
# ArchUnit for Spring Boot — Enforcing Layered Architecture as a Unit Test
|
|
|
|
Companion module for [ArchUnit for Spring Boot: Enforcing Layered Architecture as a Unit
|
|
Test](https://ankurm.com/archunit-spring-boot-layered-architecture-unit-test/) on
|
|
[ankurm.com](https://ankurm.com). Every code sample and every console transcript quoted in
|
|
that post comes from the files in this directory — nothing was hand-typed into the article.
|
|
|
|
## Versions this was built and run against
|
|
|
|
| Component | Version | Notes |
|
|
|---|---|---|
|
|
| ArchUnit | **1.5.1** | current GA per `maven-metadata.xml` on Maven Central at the time of writing |
|
|
| JUnit Jupiter / Platform | **6.1.3** | via `archunit-junit5`, which brings its own JUnit 5 extension |
|
|
| Spring Framework | 7.0.9 | `spring-context` + `spring-webmvc` only — no `spring-boot-starter-web`, no embedded server, no `ApplicationContext` anywhere in this module |
|
|
| JDK | **25 (Temurin, LTS)** | build and run; `maven.compiler.release` is set to 17 |
|
|
| Maven | 3.9.11 | |
|
|
| Maven Surefire Plugin | 3.5.2 | |
|
|
|
|
This module deliberately does not depend on any `spring-boot-starter-*` artifact. ArchUnit
|
|
reads compiled `.class` files directly — it has never needed a running Spring context, and
|
|
the whole point of the first section of the post is that none of these tests say
|
|
`@SpringBootTest`.
|
|
|
|
## Quickstart
|
|
|
|
```bash
|
|
mvn test # all four rules, no application ever starts, ~4s total
|
|
mvn test -Dtest=LayeredArchitectureTest # just the three structural rules
|
|
mvn test -Dtest=FreezingArchRuleTest # just the frozen-violation rule
|
|
```
|
|
|
|
Gradle users: there is no `build.gradle.kts` in this module (unlike this repo's other
|
|
modules) because the whole demonstration is Maven-plugin-agnostic — `archunit-junit5`
|
|
plugs into the JUnit 5 engine the same way under either build tool, and the commands above
|
|
are the part that matters.
|
|
|
|
## Source files
|
|
|
|
| File | Demonstrates |
|
|
|---|---|
|
|
| [`OrderRepository.java`](src/main/java/com/ankurm/tutorials/junit/archunit/repository/OrderRepository.java) / [`InMemoryOrderRepository.java`](src/main/java/com/ankurm/tutorials/junit/archunit/repository/InMemoryOrderRepository.java) | the repository layer — a `@Repository` with no framework beyond the stereotype annotation itself |
|
|
| [`OrderStatusService.java`](src/main/java/com/ankurm/tutorials/junit/archunit/service/OrderStatusService.java) | the service layer — constructor injection only, which is what makes `noFieldInjection` meaningful rather than cosmetic |
|
|
| [`OrderStatusController.java`](src/main/java/com/ankurm/tutorials/junit/archunit/controller/OrderStatusController.java) | the web layer — depends on the service layer only |
|
|
| [`LegacyOrderExporter.java`](src/main/java/com/ankurm/tutorials/junit/archunit/service/LegacyOrderExporter.java) | a deliberately-kept pre-existing `throws Exception` violation — the thing `FreezingArchRuleTest` freezes rather than fixes |
|
|
| [`LayeredArchitectureTest.java`](src/test/java/com/ankurm/tutorials/junit/archunit/LayeredArchitectureTest.java) | `layeredArchitecture()`, `noFields().should().beAnnotatedWith(Autowired.class)`, and `slices().should().beFreeOfCycles()` — three independent structural rules, all checked without starting anything |
|
|
| [`FreezingArchRuleTest.java`](src/test/java/com/ankurm/tutorials/junit/archunit/FreezingArchRuleTest.java) | `FreezingArchRule.freeze(...)` wrapping a rule that would otherwise fail immediately, recording today's known violations as an accepted baseline |
|
|
| [`archunit.properties`](src/test/resources/archunit.properties) | the three properties that control where the frozen-violation store lives and whether it may be created or updated |
|
|
| [`archunit_store/`](archunit_store) | the frozen-violation store itself, committed to version control — this directory **is** the baseline the rule enforces against |
|
|
|
|
## Captured output
|
|
|
|
| File | What it shows |
|
|
|---|---|
|
|
| [`00-baseline-passing-run.txt`](docs/output/00-baseline-passing-run.txt) | the three structural rules, all green, ~1.3s, no `ApplicationContext` started |
|
|
| [`01-layering-violation-failure.txt`](docs/output/01-layering-violation-failure.txt) | a real `layeredArchitecture()` failure, captured by temporarily adding a controller that bypassed the service layer, then deleting it |
|
|
| [`02-freeze-run1-baseline-captured.txt`](docs/output/02-freeze-run1-baseline-captured.txt) | the first-ever run of `FreezingArchRuleTest`: no store existed yet, so it was created with today's one known violation already accepted |
|
|
| [`03-freeze-run2-new-violation-caught.txt`](docs/output/03-freeze-run2-new-violation-caught.txt) | a second, genuinely new `throws Exception` added after the baseline was recorded — the failure names only the new offender, never the frozen one |
|
|
| [`04-full-suite-final.txt`](docs/output/04-full-suite-final.txt) | all four rules, exactly as committed, green in under four seconds |
|
|
|
|
## A diagnostic note, not a defect
|
|
|
|
`LegacyOrderExporter.export()` still declares `throws Exception` in the committed code, and
|
|
`noNewGenericExceptionsDeclared` in `FreezingArchRuleTest` still technically covers it. The
|
|
build is green anyway, on purpose: `archunit_store/` records that specific violation as
|
|
already known at the time the rule was turned on, and `freeze()` only fails a build on a
|
|
violation that is *new* since that recording. This is the realistic case — a team adopting
|
|
ArchUnit on a codebase that is not already clean — and the point the post's freezing section
|
|
makes is that you do not have to choose between "fix two hundred existing violations today"
|
|
and "don't bother enforcing the rule at all."
|
|
|
|
## License
|
|
|
|
MIT, matching the rest of this repository.
|