Writing Test
Use this skill to choose the right test level first, then write compact contract-focused Java tests.
Use JUnit APIs for assertions and test structure. Mockito is allowed when interaction matters, but keep assertions in JUnit. Do not introduce AssertJ, Hamcrest, Truth, or other assertion libraries unless the project already uses them. For E2E JSON body checks, use jsonPath; Hamcrest matchers are allowed only inside jsonPath assertions.
Workflow
- Read the target code and nearby tests before writing anything.
- Decide the lowest useful test level: unit, integration, or E2E.
- Load the matching reference only when needed:
- Unit tests: references/unit.md
- Integration tests: references/integration.md
- E2E tests: references/e2e.md
- Identify the public contract and add the smallest useful test set.
- Avoid duplicate coverage across levels.
- Put unit and integration tests in the module that owns the behavior.
- Run the narrowest relevant test command first, then broader tests when risk justifies it.
Test Levels
Choose the lowest useful level:
- Domain value object, entity helper method, use-case branch, repository adapter call shape: unit test.
- JPA entity mapping, repository query method, transaction/lifecycle behavior: integration test.
- HTTP route, serialization, security filter, controller-service-use-case-repository flow: E2E test.
Avoid duplicate coverage:
- If E2E verifies HTTP -> repository -> domain -> JSON, do not also assert every mapped domain field in adapter unit tests.
- Keep adapter unit tests only for adapter-specific policy, such as forcing
activeOnly = true on repository calls.
- Keep entity unit tests for pure entity helper contracts such as
isDeleted() and setDeleted().
Test Fixtures
- Use Gradle
java-test-fixtures only when fixtures must be shared across modules.
- Prefer fixtures in the adapter module that owns the fixture type.
- App-level E2E or integration tests may depend on
testFixtures(project(":<adapter-module>")) when they need adapter-owned entity fixtures.
- Do not create a separate fixture module for a single adapter's test data.
Common Structure
Prefer @Nested groups when a class has multiple meaningful behavior surfaces.
If there is only one meaningful group, and the class is unlikely to expand soon, keep the test cases flat in the test class. Do not create a single @Nested group only for ceremony.
Within a group or flat class, place successful/valid cases first, then boundary cases, then invalid/exception cases.
Use @BeforeEach when multiple test cases repeat the same setup or fixture creation. Keep shared preconditions in setup and behavior-specific values inside the test method.
Naming
- Prefer
shouldBe... method names.
- Use
shouldBeRejectedWhen... for invalid inputs.
- Use
shouldBeReturned...When... for getter/helper return behavior.
- Use
shouldBeThrownWhen... for expected exception paths.
- Keep detailed behavior text in
@DisplayName; match the repository's language, but Korean test cases must always end with ~해야 한다 or ~이어야 한다.
- Always write E2E with uppercase
E2E in class and file names, such as UserProfileE2ETest.
Assertions
Use assertAll when one action produces a state with multiple observable properties.
Avoid asserting exception messages unless the message is part of the public contract.
Gotchas
- Do not write null tests for non-null JSpecify contracts.
- Do not create controller unit tests only to mirror JSON response shape.
- Do not duplicate E2E coverage with repository or adapter tests unless the lower-level policy differs.
- Do not use AssertJ only because examples online use it; follow the project assertion stack.
- Do not verify JSON responses with raw string
contains; use jsonPath or parsed JSON assertions at the intended field path.
Nullability Policy
Respect JSpecify nullability contracts. Treat declared annotations as the source of truth.
- A parameter, field, or return type without
@Nullable is non-null under JSpecify defaults such as @NullMarked. Do not write tests that pass null to those parameters or expect null from those returns.
- A parameter, field, or return type with
@Nullable may be null. Cover the null case explicitly, including the policy encoded by the production code.
- If existing code is missing
@Nullable but actually accepts null, fix the source annotation first, then write the test.
- When verifying defensive behavior against a contract violation, suppress static checks with
@SuppressWarnings("DataFlowIssue") and make the intent clear in @DisplayName.
- Annotate test helpers, fakes, and fixtures with JSpecify annotations to match the production contract. Do not relax nullability in test doubles.
Review Pass
Before finishing:
- Check the chosen test level is the lowest useful level.
- Check level-specific reference rules were followed.
- Check success cases appear before failure cases.
- Check method names follow project conventions.
- Check tests cover the intended contract without duplicating higher-level coverage.
- Check
null tests exist only for declared nullable contracts or explicit misuse defense tests.
- Check E2E class/file names use uppercase
E2E.
- Check E2E JSON body assertions use
jsonPath.
- Run the narrow test command and report the result.
1---2name: writing-java-tests3description: Write, improve, or review Java tests with JUnit, Mockito, Spring Boot, Spring Data JPA, and JSpecify across unit, integration, and E2E levels. Use only for Java test work, including domain, use-case, entity, validation, repository, adapter, HTTP API, nullability, or Java test-level selection decisions.4---56# Writing Test78Use this skill to choose the right test level first, then write compact contract-focused Java tests.910Use JUnit APIs for assertions and test structure. Mockito is allowed when interaction matters, but keep assertions in JUnit. Do not introduce AssertJ, Hamcrest, Truth, or other assertion libraries unless the project already uses them. For E2E JSON body checks, use `jsonPath`; Hamcrest matchers are allowed only inside `jsonPath` assertions.1112## Workflow13141. Read the target code and nearby tests before writing anything.152. Decide the lowest useful test level: unit, integration, or E2E.163. Load the matching reference only when needed:17 - Unit tests: [references/unit.md](references/unit.md)18 - Integration tests: [references/integration.md](references/integration.md)19 - E2E tests: [references/e2e.md](references/e2e.md)204. Identify the public contract and add the smallest useful test set.215. Avoid duplicate coverage across levels.226. Put unit and integration tests in the module that owns the behavior.237. Run the narrowest relevant test command first, then broader tests when risk justifies it.2425## Test Levels2627Choose the lowest useful level:2829- Domain value object, entity helper method, use-case branch, repository adapter call shape: unit test.30- JPA entity mapping, repository query method, transaction/lifecycle behavior: integration test.31- HTTP route, serialization, security filter, controller-service-use-case-repository flow: E2E test.3233Avoid duplicate coverage:3435- If E2E verifies HTTP -> repository -> domain -> JSON, do not also assert every mapped domain field in adapter unit tests.36- Keep adapter unit tests only for adapter-specific policy, such as forcing `activeOnly = true` on repository calls.37- Keep entity unit tests for pure entity helper contracts such as `isDeleted()` and `setDeleted()`.3839## Test Fixtures4041- Use Gradle `java-test-fixtures` only when fixtures must be shared across modules.42- Prefer fixtures in the adapter module that owns the fixture type.43- App-level E2E or integration tests may depend on `testFixtures(project(":<adapter-module>"))` when they need adapter-owned entity fixtures.44- Do not create a separate fixture module for a single adapter's test data.4546## Common Structure4748Prefer `@Nested` groups when a class has multiple meaningful behavior surfaces.4950If there is only one meaningful group, and the class is unlikely to expand soon, keep the test cases flat in the test class. Do not create a single `@Nested` group only for ceremony.5152Within a group or flat class, place successful/valid cases first, then boundary cases, then invalid/exception cases.5354Use `@BeforeEach` when multiple test cases repeat the same setup or fixture creation. Keep shared preconditions in setup and behavior-specific values inside the test method.5556## Naming5758- Prefer `shouldBe...` method names.59- Use `shouldBeRejectedWhen...` for invalid inputs.60- Use `shouldBeReturned...When...` for getter/helper return behavior.61- Use `shouldBeThrownWhen...` for expected exception paths.62- Keep detailed behavior text in `@DisplayName`; match the repository's language, but Korean test cases must always end with `~해야 한다` or `~이어야 한다`.63- Always write E2E with uppercase `E2E` in class and file names, such as `UserProfileE2ETest`.6465## Assertions6667Use `assertAll` when one action produces a state with multiple observable properties.6869Avoid asserting exception messages unless the message is part of the public contract.7071## Gotchas7273- Do not write null tests for non-null JSpecify contracts.74- Do not create controller unit tests only to mirror JSON response shape.75- Do not duplicate E2E coverage with repository or adapter tests unless the lower-level policy differs.76- Do not use AssertJ only because examples online use it; follow the project assertion stack.77- Do not verify JSON responses with raw string `contains`; use `jsonPath` or parsed JSON assertions at the intended field path.7879## Nullability Policy8081Respect JSpecify nullability contracts. Treat declared annotations as the source of truth.8283- A parameter, field, or return type without `@Nullable` is non-null under JSpecify defaults such as `@NullMarked`. Do not write tests that pass `null` to those parameters or expect `null` from those returns.84- A parameter, field, or return type with `@Nullable` may be `null`. Cover the `null` case explicitly, including the policy encoded by the production code.85- If existing code is missing `@Nullable` but actually accepts `null`, fix the source annotation first, then write the test.86- When verifying defensive behavior against a contract violation, suppress static checks with `@SuppressWarnings("DataFlowIssue")` and make the intent clear in `@DisplayName`.87- Annotate test helpers, fakes, and fixtures with JSpecify annotations to match the production contract. Do not relax nullability in test doubles.8889## Review Pass9091Before finishing:9293- Check the chosen test level is the lowest useful level.94- Check level-specific reference rules were followed.95- Check success cases appear before failure cases.96- Check method names follow project conventions.97- Check tests cover the intended contract without duplicating higher-level coverage.98- Check `null` tests exist only for declared nullable contracts or explicit misuse defense tests.99- Check E2E class/file names use uppercase `E2E`.100- Check E2E JSON body assertions use `jsonPath`.101- Run the narrow test command and report the result.