name: unit-test-pattern description: Sam's shape for JUnit Jupiter and Mockito unit tests — mock field ordering, given/when/then phases, matchers in when() and real values in verify(). Load before writing or editing any *Test.java in this repo.
Unit test pattern
Every unit test in this repo copies the shape of ControllerTest.java. JUnit Jupiter for assertions,
Mockito for collaborators, three named phases, one mock field block in a fixed order.
This skill governs *Test.java only. *IT.java runs under failsafe against Testcontainers and is a
different animal — nothing here applies to it.
The reference test
src/test/java/io/github/samrjthompson/registryatlas/companies/api/ControllerTest.java:
@ExtendWith(MockitoExtension.class)
public class ControllerTest {
@Mock
private Service service;
@InjectMocks
private Controller controller;
@Test
void shouldRetrieveCompaniesWithinFiveMileRadiusOfPostcode() {
// given
final String postcode = "G12 8AU";
final long radius = 5L;
when(service.getCompanies(anyString(), anyLong())).thenReturn(List.of());
// when
final ResponseEntity<List<String>> actual = controller.getCompanies(postcode, radius);
// then
assertEquals(HttpStatusCode.valueOf(HttpStatus.OK.value()), actual.getStatusCode());
assertEquals(List.of(), actual.getBody());
verify(service).getCompanies(postcode, radius);
}
}
Field ordering
Three groups in this order, separated by a single blank line. Never put a blank line inside a group.
@Mock
private Service service;
@Mock
private Repository repository;
@InjectMocks
private Controller controller;
@Mock
private CompanyProfile companyProfile;
- Constructor dependencies of the class under test.
- The class under test.
- General mocks — every mock that is not a constructor dependency: a mocked argument passed to the method under test, or a mocked object a stub hands back.
Not this — a blank line between every @Mock loses the grouping the layout exists to show:
@Mock
private Service service;
@Mock
private Repository repository;
@InjectMocks
private Controller controller;
@Mock
private CompanyProfile companyProfile;
Constants go above all three groups, as in any Java class.
The grouping is for the reader. @InjectMocks injects by type and ignores field position, so a general
mock whose type happens to match a constructor parameter is still injected — position does not prevent
it.
When @InjectMocks will not do
If the constructor takes a String or another plain value Mockito cannot supply, drop @InjectMocks.
Declare the field bare in the same middle position and build it in @BeforeEach void setUp(). The real
dependencies keep their @Mock:
@ExtendWith(MockitoExtension.class)
public class ServiceTest {
private static final String API_KEY = "api-key";
@Mock
private Client client;
@Mock
private Repository repository;
private Service service;
@Mock
private CompanyProfile companyProfile;
@BeforeEach
void setUp() {
service = new Service(client, repository, API_KEY);
}
}
The three phases
Every test body is // given, // when, // then, each marker preceded by a blank line. This is the
normal case, and most tests look exactly like it.
These markers are the sole exception to the no-code-comments skill. They mark structure rather than
explain code, so they cannot rot. Nothing else in a test file may carry a comment — if a step needs
explaining, rename the variable or the test method until it does not.
Folding two phases together
Where a phase would stand completely empty, fold it into its neighbour and combine the two markers.
Fold given into when only when there is nothing whatever to arrange — the method under test takes no
arguments, or takes only mocks already declared at class level. Declaring a local is arranging, so a
test carrying even one final String postcode = "G12 8AU"; keeps its own // given:
@Test
void shouldPassSuppliedCompanyToService() {
// given / when
controller.save(companyProfile);
// then
verify(service).save(companyProfile);
}
Fold when into then when the call under test happens inside the assertion, leaving no separate
invocation to mark:
@Test
void shouldPropagateExceptionWhenPostcodeIsUnknown() {
// given
final String postcode = "ZZ99 9ZZ";
final long radius = 5L;
when(service.getCompanies(anyString(), anyLong())).thenThrow(new PostcodeNotFoundException());
// when / then
assertThrows(PostcodeNotFoundException.class, () -> controller.getCompanies(postcode, radius));
}
assertDoesNotThrow(() -> controller.getCompanies()) folds the same way.
Three separate markers stay the default. Reach for a combined marker because the test came out that shape, not to tighten a test that already reads fine.
Stubbing and verification
Real values never appear inside when(). They appear in the verify(…) arguments in the then step,
which is what pins the contract:
// given
final String postcode = "G12 8AU";
final long radius = 5L;
when(service.getCompanies(anyString(), anyLong())).thenReturn(List.of());
// then
verify(service).getCompanies(postcode, radius);
Never when(service.getCompanies("G12 8AU", 5L)).
Reach for the type-specific matcher first — anyString(), anyLong(), anyInt(), anyList(). Use
any(Foo.class) or eq(Foo.class) only where the extra specificity is genuinely required, such as
picking between overloads or matching a class-literal argument:
when(restClient.get(anyString(), eq(CompanyProfile.class))).thenReturn(companyProfile);
Mockito is all-or-nothing about matchers: once one argument is a matcher, every argument must be. That
is the only reason eq() exists — not as a way to smuggle a real value into a stub.
Assertions
JUnit Jupiter only, statically imported from org.junit.jupiter.api.Assertions — assertEquals,
assertThrows, assertTrue, and the rest. AssertJ and Hamcrest are not on the classpath and adding
either needs Sam's agreement first.
Static imports throughout: assertEquals, not Assertions.assertEquals; when and verify, not
Mockito.when and Mockito.verify.
Naming and modifiers
- Class is
public, named<ClassUnderTest>Test, annotated@ExtendWith(MockitoExtension.class). - Test methods are package-private and
void. Nopublic. - Method names start
shouldand carry both the behaviour and the condition:shouldRetrieveCompaniesWithinFiveMileRadiusOfPostcode, nottestGetCompanies. - Locals are
final. - The result of the call under test is named
actual.
TDD Red-Green-Refactor
Testing
Skill qui guide Claude a travers le cycle TDD complet.
Audit d'Accessibilité Web
Testing
Réalise un audit d'accessibilité web complet selon les normes WCAG.
Générateur de Tests UAT
Testing
Génère des cas de test d'acceptation utilisateur structurés et complets.