Files
skills/skills/tdd/references/java.md
T
mathias 8da7c9c45f docs(tdd): make the spine language-neutral, add four language references
The discipline in tdd was already language-agnostic; the checklist under it was
not. A Python user inherited a mandatory `go test -race` box they can never tick
and never got told that `mypy --strict` belongs in the red gate.

Per #18's own comment and writing-skills' Multi-Language Dilution warning, the
main SKILL.md keeps ONE canonical inline example (Go, the default stack) and the
other four languages live in reference files: python, typescript, rust, java.

The RED run drove every change. A subagent given a Python task with the Go-only
skill reported, in its own words:

- "Everything from L169 to L244 is Go-only, and the verification checklist at
  L326 silently inherits that Go-ness — which is why a Python user ends up with
  a mandatory box (-race) they can never tick and a required gate (mypy
  --strict) that isn't listed at all."
- "For a brand-new function, the first RED is always a collection-time
  ImportError... The only clean resolutions are a stub that raises
  NotImplementedError (production code before a red test?) or accepting
  ImportError as a legitimate red. The skill picks neither, and this is the
  single biggest gap." It now picks: that red is legitimate, and writing a stub
  to satisfy the runner is the Iron Law with extra steps.
- The Go table-driven exemplar branches on a `wantErr` column inside the loop.
  Translated literally that is a conditional assertion folding two behaviours
  into one test — which this skill's own quality table forbids. The agent caught
  itself mid-translation; both SKILL.md and python.md now say not to port it.

Also: the checklist names the static-analysis gate per language instead of
assuming `go vet`, and says to strike the concurrency box deliberately where no
equivalent exists — a box that cannot be ticked teaches people to skip
checklists.

Refs #18
Bump-Type: minor
2026-08-29 14:10:10 +02:00

3.0 KiB

TDD in Java

The Iron Law and Red-Green-Refactor in SKILL.md apply unchanged. This file replaces the Go-Specific TDD Notes for Java. JUnit 5 (Jupiter) throughout.

The first red is a compile error, and that is correct

cannot find symbol: method splitVat is a legitimate red. Do not stub the method with throw new UnsupportedOperationException() to make the module compile — that is production code before a failing test.

Table-driven: @ParameterizedTest

@ParameterizedTest(name = "{0}")
@MethodSource("vatCases")
void roundsHalfToEven(String name, long amountMinor, BigDecimal rate, long net, long vat) {
    assertEquals(new Split(net, vat), VatSplitter.split(amountMinor, rate));
}

static Stream<Arguments> vatCases() {
    return Stream.of(
        Arguments.of("tie rounds down to even", 14L,    new BigDecimal("0.12"), 12L,    2L),
        Arguments.of("tie rounds up to even",   42L,    new BigDecimal("0.12"), 38L,    4L),
        Arguments.of("exact division",          12500L, new BigDecimal("0.25"), 10000L, 2500L)
    );
}

name = "{0}" puts the case label in the report — the t.Run analogue. @CsvSource is terser for primitives; @MethodSource is the one to use as soon as a case carries a BigDecimal or a domain object.

Error cases get their own test

@Test
void rejectsNegativeAmount() {
    var e = assertThrows(IllegalArgumentException.class,
                         () -> VatSplitter.split(-1, new BigDecimal("0.25")));
    assertTrue(e.getMessage().contains("non-negative"));
}

Assert on the message, not only the type — IllegalArgumentException is thrown by half the JDK, and a test that accepts any of them will keep passing for the wrong reason.

Money is BigDecimal or long minor units, never double

0.1 + 0.2 != 0.3 is not a rounding curiosity in a payments system, it is a defect. Where a rounding mode matters, state it: setScale(0, RoundingMode.HALF_EVEN). Banker's rounding is the default expectation in Swedish accounting and is not Java's default.

Assertions

Prefer AssertJ (assertThat(x).isEqualTo(y)) where the project already has it: the failure messages name the actual value. Where it does not, plain JUnit assertions are fine — do not add a dependency for aesthetics.

assertAll is the closest analogue to Go's soft assert: it reports every failure in the group instead of stopping at the first.

Mocks

Mockito is standard, and easy to overuse. Per SKILL.md, mock only across a system boundary. A mocked class you own tests the mock; a @SpringBootTest that mocks the repository tests neither the wiring nor the query.

Gates

./gradlew test            # the failing test
./gradlew check           # static gate: compiler warnings, spotless, errorprone

Java has no -race. For concurrent code the honest substitutes are a stress test with CountDownLatch to force the interleaving, and jcstress for lock-free work. A pure function should strike that checklist box deliberately.