Testing Guidelines¶
Principles¶
- Test cases are independent — no shared mutable state between tests.
- Test public interfaces, not internal implementation.
- Unit-test the happy path and all boundary cases.
- Tests are deterministic — no
sleep, no wall-clock time, no unfixed random seeds. - Inject clocks, UUID generators etc. that are controllable by the test driver
- No network or file-system access in unit tests.
Test Pyramid (Martin Fowler)¶
- Unit tests — fast, isolated, the majority. Mock only at architectural boundaries.
- Integration tests — test component interactions at boundaries; always use BDD; may be written in a different language (e.g., Python/Behave).
- End-to-end tests — minimal; critical user journeys only.
BDD¶
- Use cases are covered by BDD integration tests.
- Structure:
Given/When/Then. - Preferred frameworks: Godog (Go), Cucumber + JUnit 5 (Java), Behave (C++ acceptance tier).
- C++ has no maintained native Gherkin runner (
cucumber-cpphas been effectively unmaintained for years), so Behave drives the.featurefiles over HTTP against a running, coverage-instrumented instance (make bdd-test) instead of a native equivalent. Catch2 remains for C++ unit/component tests that need no live process — the two are complementary: Catch2 tests objects directly, Behave exercises the same binary the way a real client would. - The feature files in this repository are meant as documentation and executable tests. They are primarily serving the developers, in order to support development. They are executed together with the unit tests, if possible. Therefore, in this repository, they must not be understood as actuale end-to-end tests, but rather as more expressive unit and component tests. As a result, mocks might be used instead of actual infrastructure components, in order to keep the tests fast and focused on the logic of the use cases.
Real-Infrastructure Tests (Component Tier)¶
Some correctness (ORM/schema mapping, migration behavior) can only be verified against a real dependency — mocking it would defeat the point. When this is unavoidable, isolate it into its own tier, separate from the default test target, so the fast unit/BDD loop above never needs Docker or any other external service. Tag a test by what it needs, not by which filename pattern happens to match a particular plugin — one filtering mechanism, driven by one CLI-overridable property, beats juggling several plugins with their own naming conventions:
- Name and invoke it separately (e.g.
make component-test) — never let it run as part ofmake test/mvn test, and never let it gate the same fast feedback loop BDD/unit tests do. - Java: tag the class
@Tag("component")(JUnit 5) and filter via Maven Surefire's<groups>/<excludedGroups>, bound to a project property (<properties><excludedGroups>component</excludedGroups></properties>+<configuration><excludedGroups>${excludedGroups}</excludedGroups></configuration>— a literal value instead of a property is not CLI-overridable). The dedicated target overrides it:mvn test -DexcludedGroups= -Dgroups=component. Avoid naming the class*IT.java,IT*.java, or*ITCase.java:spring-boot-starter-parentbinds Maven Failsafe'sintegration-test/verifygoals unconditionally for every child POM (even ifmaven-failsafe-pluginis never declared), so a class matching those patterns would still be picked up and re-run a second time under Failsafe's own defaults duringmvn verify— bypassing the tag filter entirely and pulling Docker back intomake coverage. A plain*Test.java/*ComponentTest.javaname sidesteps this; the@Tagis what actually governs inclusion, the name is just a human-readable hint. - Not every JUnit 5 extension honors
@Tag: some libraries (e.g. ArchUnit'sarchunit-junit5) register their own JUnit PlatformTestEnginethat does not expose@Tagto Surefire'sgroups/excludedGroupsfilter (confirmed empirically —-Dgroups=<tag>produced zero executions of an ArchUnit test tagged that way). For a test whose engine doesn't honor tags, fall back to filename-based Surefire<excludes>for the default run plus an explicit-Dtest=<ClassName>for its dedicated target, instead of assuming tags work universally. - Exclude the code this tier alone exercises (e.g. JPA entities, repository adapters) from the coverage gate, so the threshold (see ci_cd.md) stays reachable without Docker.
- Go/C++: the same principle applies via the language's native tag mechanism — build tags or
Godog's
--tags(Go), Catch2's[tag]/--tag-searchor Behave's--tags(C++) — driving an equivalent separate, Docker-requiring target; never fold it intomake test.
Mocking¶
- Mock only at layer boundaries (e.g.,
infrainterfaces defined inusecase). - Never mock domain or use-case logic — test it directly.
Test Data¶
- Use builders or factories; avoid large inline literals.
- Never depend on test execution order.
- Every value passed to a Gherkin step must be asserted by a
Thenor exercise a business rule. If a domain field only exists to satisfy this pattern with no real behavior behind it, that's a YAGNI signal — remove the field instead of keeping it around with placeholder test data.
Directives¶
- Test public interfaces, not internal implementation; tests are independent and deterministic
- No
sleep, wall-clock time, or unfixed random seeds; inject clocks and UUID generators - No network or file-system access in unit tests
- Mock only at architectural layer boundaries (infra interfaces defined in usecase); never mock domain logic
- Every use case must have a BDD scenario (Godog for Go, Cucumber+JUnit for Java, Behave for C++ acceptance tests — Catch2 covers C++ unit/component tests)
- Use builders or factories for test data; never depend on test execution order
- Every Gherkin step parameter must be asserted by a Then or exercise a business rule; if a domain field exists only to satisfy this pattern with no real behavior behind it, remove the field (YAGNI) instead of keeping it around with placeholder test data
- BDD tests contained herein are meant for developers. They are not meant as actual end-to-end tests. Therefore, mocks may be used instead of actual infrastructure components
- Real-infrastructure component tests (e.g. Testcontainers) are tagged by what they need (e.g.
@Tag("component")) and filtered via a CLI-overridable property, not by filename pattern; they get their own separate target (e.g.make component-test), nevermake test/mvn test/mvn verify— exclude the code they alone cover from the coverage gate instead of pulling Docker into the default test tier - When a test's own JUnit Platform engine doesn't honor
@Tag(verify empirically rather than assuming), fall back to filename-based Surefire<excludes>/-Dtest=selection for that tier