Skip to content

Testing — JUnit 5 + Mockito

Spring Boot testing is organized in three layers, each with its own scope and tooling:

Layer Annotation Type Database
Repository @DataJpaTest Integration H2 in-memory
Service @SpringBootTest Unit Mocked
Controller @WebMvcTest Web layer slice Mocked

Repository Testing

@DataJpaTest spins up an H2 in-memory database and loads only the JPA layer (no controllers, no services). This makes it an integration test — you're testing real SQL queries against a real (in-memory) database engine, not mocking anything.

You can place a data.sql file in src/test/resources/ to pre-populate the database before tests run.

@DataJpaTest
public class UserRepositoryTest {
    @Autowired
    private UserRepository userRepository;

    @Test
    void shouldGetAllUsers() {
        assertEquals(3, userRepository.findAll().size());
    }
}

@Autowired is the right choice here — Spring manages the repository bean inside the test context, so you ask Spring to inject it rather than constructing it yourself.


Service Testing

This is a unit test: you isolate the service and replace its dependency (the repository) with a mock. The goal is to verify the service's logic, not the database.

@SpringBootTest
@ActiveProfiles("test")
class UserServiceTest {

    @MockitoBean
    private UserRepository userRepository;

    @Autowired
    private UserService userService;

    private User u1 = new User(1L, "Luke", "Skywalker");
    private User u2 = new User(2L, "Darth", "Vader");

    @Test
    void shouldReturnAllUsers() {
        when(userRepository.findAll()).thenReturn(List.of(u1, u2));

        List<User> users = userService.getAllUsers();

        assertEquals(2, users.size());
    }
}

@MockitoBean replaces the UserRepository bean in the Spring context with a Mockito mock. The service gets this mock injected — no real database is ever touched.

@ActiveProfiles("test") tells Spring to load application-test.yml instead of the default application.yml. Even though the repository is mocked, @SpringBootTest still loads the full application context. Without a test profile, Spring might try to connect to your production datasource during startup. With application-test.yml you can point to H2 or disable the datasource entirely to keep tests self-contained.

@Autowired on the service is correct here. In a test class, you're not constructing the object yourself — you're asking Spring's context for the bean it already built. Constructor injection is for production code; @Autowired is the standard way to get beans in @SpringBootTest tests.

u1 and u2 are defined as fields on the test class so they can be shared across multiple test methods. when(...).thenReturn(...) tells the mock how to respond when that method is called — it doesn't hit the database, it just returns your predefined objects.


Controller Testing

This is a web layer slice test — not end-to-end. True E2E would involve a real running server, a real database, and real HTTP over the network. Here, @WebMvcTest loads only the controller layer (no database, no full Spring context) and MockMvc dispatches requests directly to the DispatcherServlet in-process, bypassing the network entirely.

What you're verifying: the controller calls the right service method, maps to the right URL, returns the right HTTP status code, and serializes the response body correctly.

@WebMvcTest(UserController.class)
public class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private UserService userService;

    private User u1 = new User(1L, "Luke", "Skywalker");
    private User u2 = new User(2L, "Darth", "Vader");

    @Test
    void shouldReturnAllUsers() throws Exception {
        when(userService.getAllUsers()).thenReturn(List.of(u1, u2));

        mockMvc.perform(get("/api/users"))
            .andExpect(status().isOk())
            .andExpect(jsonPath("$[0].lastname").value("Skywalker"));
    }
}

MockMvc is automatically registered as a bean by @WebMvcTest, so @Autowired works. It lets you simulate GET, POST, etc. and inspect the response (status, headers, JSON body) without starting a real HTTP server.

jsonPath("$[0].lastname") uses JSONPath syntax to navigate the response body: $ is the root, [0] is the first element of the array, .lastname is the field. Useful to assert specific values in the returned JSON without deserializing the whole response.