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
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.