Pattern de test unitaire JUnit/Mockito

Modèle de tests unitaires Java avec JUnit Jupiter et Mockito : ordre des mocks, phases given/when/then, matchers dans when() et valeurs réelles dans verify().

Spar Skills Guide Bot
TestingIntermédiaire
8013/08/2026
Claude CodeCursorWindsurfCopilotCodex
#unit-testing#junit-jupiter#mockito#java#test-pattern

Recommandé pour


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;
  1. Constructor dependencies of the class under test.
  2. The class under test.
  3. 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.AssertionsassertEquals, 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. No public.
  • Method names start should and carry both the behaviour and the condition: shouldRetrieveCompaniesWithinFiveMileRadiusOfPostcode, not testGetCompanies.
  • Locals are final.
  • The result of the call under test is named actual.
Skills similaires