Skip to content
Back to skills

Java Test Design

ASecurity

Writing a Java test that survives refactoring and says why it failed: naming the behaviour rather than the method, one reason to fail, test data builders over shared mutable setup, choosing the assertion that produces a readable failure, parameterised and nested tests, and controlling relevant inputs — clock, ordering, locale, randomness. Use when a test name does not say what broke, when a failure message has to be decoded by reading the test, when setup is shared across unrelated tests, whe...

  • 2 stars
  • 0 votes
  • 0 copies
  • 3 views
  • Added September 19, 2026
developmentgojavatestingrefactoringgitapi

Works with

  • api

Security analysis

A100/100

Pro scans all 4 files and shows the line behind each finding

Scanned September 29, 2026

npx -y skills add robsonkades/agent-skills --skill java-test-design --agent claude-code

Installs into .claude/skills of the current project.

Are you the author of Java Test Design?

Add the live security badge to your README. It updates with every re-scan.

Security grade badge for Java Test Design
[![Security: A — Skills Directory](https://www.skillsdirectory.com/api/skills/robsonkades-java-test-design/badge)](https://www.skillsdirectory.com/skills/robsonkades-java-test-design)

More formats (shields.io, HTML) on the badges page. Keep it an A: scan every change in CI with Pro.

Download with Pro
SKILL.md
---
name: java-test-design
description: >
  Writing a Java test that survives refactoring and says why it failed: naming the
  behaviour rather than the method, one reason to fail, test data builders over shared
  mutable setup, choosing the assertion that produces a readable failure, parameterised and
  nested tests, and controlling relevant inputs — clock, ordering,
  locale, randomness. Use when a test name does not say what broke, when a failure message
  has to be decoded by reading the test, when setup is shared across unrelated tests, when
  a test sleeps, when tests pass alone and fail together, when a flaky test is about to be
  retried or disabled, or when the same assertions are being copied across cases. Does not
  cover which level to test at (java-testing-strategy), stubs and mocks (java-test-doubles),
  the red-green-refactor loop (tdd), or threading (concurrency-testing).
---

# Java Test Design

## Purpose

A failing test has one job: tell you what broke without being read. Most tests fail that
job — the name repeats the method name, the message says `expected: true but was: false`,
and the arrangement is thirty lines of setup shared with tests that need none of it.

The second job is surviving. A test bound to how the code is structured must be rewritten by
every refactoring, and a suite that must be rewritten by every refactoring is a suite that
gets deleted the first time a deadline arrives.

## Workflow

Inspect the project's JDK/toolchain, resolved Jupiter/assertion versions, lifecycle/parallel
configuration and existing test command first. Reference snippets were authored for JDK 25,
use Java 17+ syntax and Jupiter 5 APIs, and do not authorize upgrades or new dependencies. When a
failure cannot be reproduced, report the observation and diagnostic next step, not a guessed cause.

1. **Name it after the condition and the expected behaviour**, so a reader can predict the
   assertion from the name alone: `renewalOneDayAfterTheWindowIsNotDue`, not
   `testIsDueWithin2`. Method names or digits are fine when they help describe the contract.
2. **Give it one reason to fail.** Multiple assertions are fine when they describe one
   outcome; two unrelated outcomes are two tests, because the first failure hides the second.
3. **Keep the relevant arrangement visible.** Direct construction is often enough. For
   recurring incidental setup, a builder with sensible defaults can expose the changing input:
   `aSubscription().renewingOn(MARCH_9).build()`. Reuse adequate project helpers and assertions.
4. **Choose the assertion for its failure message.** `assertThat(list).containsExactly(a, b)`
   prints both lists on failure; `assertTrue(list.equals(...))` prints `false`.
5. **Control the inputs relevant to the contract** — clock, order, locale/zone, randomness and
   external resources. Preserve required integration and default-environment behavior by
   isolating/configuring it, rather than changing production semantics to simplify the test.
   See `references/determinism.md`.
6. **For a consequential new regression test, check a representative fault** with a temporary
   local mutation or the known failing revision. Confirm the intended assertion detects it;
   compilation, discovery or unrelated setup failure is not evidence of that detection.
   Restore the mutation and rerun the test; inspect diagnostic clarity. Do not leave broken
   production code.

## Rules

- Keep scenario selection explicit: parameterise data-only cases instead of branching to choose
  unrelated assertions. Loops, generated cases and property assertions are valid when their oracle
  is independent, readable and identifies the failing input.
- One behaviour per test; `assertAll` only for several facets of the _same_ outcome, so that
  all of them are reported rather than just the first.
- Verify the intended scenario reaches its checks. A loop, `allMatch` or `assertAll` over an
  unexpectedly empty result can pass without checking an element. Assert required membership
  or cardinality from the contract; when emptiness is the expected outcome, assert that instead.
  See the execution checks in `references/junit5-patterns.md`.
- Shared mutable fixture state is a lead for "passes alone, fails together", not proof of the
  cause. Prefer fresh test/`@BeforeEach` state; deliberate sharing needs a verified reset,
  ownership and synchronization contract. `PER_CLASS` reuses one instance; `PER_METHOD` does
  not isolate static/external state or references to shared objects.
- Do not use an arbitrary sleep as proof of readiness or completion. Await the relevant
  milestone with a bound and cleanup. An intentional delay can belong to a temporal fault
  model, but does not establish that another task finished (concurrency-testing).
- Do not derive the expectation by repeating the behavior under test; both copies can be
  wrong together. Use explicit example values or an independent oracle/property. Sharing
  incidental value constructors is different from copying the algorithm being tested.
- Assert on the resulting value whenever the outcome is observable as one. Verifying that a
  collaborator was called is a claim about implementation, and is only justified when the
  call _is_ the outcome (java-test-doubles).
- Assert the exception type always, and its message only when the message is part of the
  contract callers rely on. Put only the intended operation inside the exception assertion;
  fallible arrangement there can satisfy it before the operation runs. `assertThrows`
  accepts subtypes; require an exact class only when the contract does, using an API available
  in the project (see `references/junit5-patterns.md`). A manual `try/fail/catch` must also fail
  on no-throw and wrong-type paths.
- Parameterise cases sharing an arrangement and assertion contract. A conditional in an
  independent oracle does not by itself require separate tests; split materially different
  behaviors when grouping obscures the scenario or failure.
- A flaky test is a defect report about the test, code or environment. Preserve the regression
  signal: do not delete, weaken or disable it just to pass. Bounded repetition can diagnose a
  flake if every outcome is retained; retry-until-green is not a fix.
- Assertion helpers/custom assertions are useful for recurring domain contracts when names,
  actual/expected values and caller context make failures clear. Avoid helpers that hide which
  behaviour is asserted or duplicate production logic.

Report the behaviour covered, relevant boundary/failure cases, exact command and executed test
count, and any untested hypothesis. Check skipped/aborted outcomes for the intended cases;
assumption-aborted tests and a green command with zero matching tests do not validate them.
For a findings-only review, report corrections without applying them. Stop when the requested
behavior and its representative failure are supported, or identify the missing evidence.

## References

- **JUnit patterns** — `references/junit5-patterns.md`. Partial examples
  (Java 17+ syntax, Jupiter 5 APIs): test data builder, `@ParameterizedTest` with `@CsvSource` and implicit
  `java.time` conversion, `@Nested` for context, exception assertions, and the lifecycle
  choices that create shared state. Read when reaching for a Jupiter feature or checking
  whether a passing test exercised its intended case.
- **Removing non-determinism** — `references/determinism.md`. The controllable inputs a test
  accidentally depends on — clock, zone, locale, charset, iteration order, randomness,
  filesystem, ports — each with the substitution, plus the "passes alone, fails together"
  checklist. Read when a test is flaky or order-dependent.

Files in this skill

  • SKILL.md6.6 KB
  • references/determinism.md6.4 KB
  • references/junit5-patterns.md8.1 KB
  • skill.yaml1.8 KB

Attribution

Is this your skill, or is something wrong with this listing? Request removal or report an issue. Author removals are honored within 72 hours.

Comments

Loading comments…

Newsletter

Get the weekly list of safe new skills, and the ones we pulled.