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
79 lines
3.0 KiB
Markdown
79 lines
3.0 KiB
Markdown
# 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`
|
|
|
|
```java
|
|
@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
|
|
|
|
```java
|
|
@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
|
|
|
|
```bash
|
|
./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.
|