Spring Boot Testing


1. Why Spring Boot Testing is Different

In pure unit testing:

  • We test plain Java classes.
  • No Spring context is loaded.

In Spring Boot applications:

  • Beans are managed by Spring.
  • Dependencies are injected automatically.
  • Web layer, security, JPA, etc. are auto-configured.

So we need different testing strategies depending on what layer we test.


2️. Spring Boot Testing Strategy (Layer-Based)

Layer Testing Approach
Controller @WebMvcTest
Service Unit Test + Mockito
Repository @DataJpaTest
Full App @SpringBootTest

This avoids loading unnecessary parts of the application.


3️. @SpringBootTest

Loads the full Spring application context.

@SpringBootTest
class ApplicationTest {
}

What It Does:

  • Starts entire Spring container
  • Loads all beans
  • Applies configuration
  • Can start embedded server

Web Environment Options

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.MOCK)

Available modes:

  • MOCK → Mock servlet environment (no real server)
  • RANDOM_PORT → Starts server on random port
  • DEFINED_PORT → Uses configured port
  • NONE → No web environment

When To Use

  • Full integration tests
  • End-to-end backend testing
  • Testing configuration behavior
  • ⚠ Slowest type of Spring test.

4️. @WebMvcTest (Controller Testing)

Used to test only the web layer.

@WebMvcTest(UserController.class)
class UserControllerTest {
}

What It Loads:

  • Controller
  • MockMvc
  • Jackson
  • MVC infrastructure

What It Does NOT Load

  • Service layer
  • Repository layer
  • Full application context

You must mock dependencies.

Example

@WebMvcTest(UserController.class)
class UserControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private UserService userService;

    @Test
    void getUser_shouldReturnUser() throws Exception {

        when(userService.getUser(1L))
                .thenReturn(new User(1L, "John"));

        mockMvc.perform(get("/users/1"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("John"));
    }
}

5️. MockMvc

MockMvc simulates HTTP requests without starting a real server.

Example:

mockMvc.perform(get("/users/1")).andExpect(status().isOk());

Common methods:

  • get()
  • post()
  • put()
  • delete()

Common assertions:

  • status()
  • jsonPath()
  • content()
  • header()

6️. @DataJpaTest (Repository Testing)

Used to test JPA layer only.

@DataJpaTest
class UserRepositoryTest {
}

What It Loads

  • JPA components
  • EntityManager
  • Repositories
  • In-memory database (H2 by default)

Example

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Test
    void saveUser_shouldPersist() {

        User user = new User(null, "John");
        userRepository.save(user);

        Optional<User> result = userRepository.findById(user.getId());

        assertTrue(result.isPresent());
    }
}

7️. @MockBean vs @Mock

Annotation Used In
@Mock Pure unit test (Mockito only)
@MockBean Spring Boot test

@MockBean replaces a bean inside Spring context.

Example:

@MockBean
private UserService userService;

Used inside:

  • @WebMvcTest
  • @SpringBootTest

8️. Testing with Security

If Spring Security is enabled, endpoints require authentication.

For controller tests:

@AutoConfigureMockMvc(addFilters = false)

This disables security filters.

Or use:

@WithMockUser

Example:

@Test
@WithMockUser(roles = "ADMIN")
void securedEndpoint_shouldAllowAccess() {
}

9️. Full Integration Example


@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class UserIntegrationTest {

    @Autowired
    private TestRestTemplate restTemplate;

    @Test
    void fullFlowTest() {

        ResponseEntity<User> response =
                restTemplate.getForEntity("/users/1", User.class);

        assertEquals(HttpStatus.OK, response.getStatusCode());
    }
}

This starts real server on random port.


10. Best Practices

  • Avoid @SpringBootTest unless needed.
  • Mock only external dependencies.
  • Keep controller tests focused on request/response.
  • Keep service tests pure unit tests.
  • Use TestContainers for realistic DB tests.
  • Avoid hitting real external APIs.

11. Professional Microservice Testing Flow

  • Controller → @WebMvcTest
  • Service → Unit Test (Mockito)
  • Repository → @DataJpaTest
  • Full API Flow → @SpringBootTest
  • External DB → TestContainers
  • External APIs → MockServer / WireMock


package com.branch.controller;

import com.branch.entity.Branch;
import com.branch.entity.BranchDTO;
import com.branch.service.AsyncUseCase;
import com.branch.service.BranchService;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.bean.override.mockito.MockitoBean;
import org.springframework.test.web.servlet.MockMvc;

import java.util.List;

import static org.hamcrest.Matchers.is;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.put;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.jsonPath;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@WebMvcTest(BranchController.class)
class BranchController_WebMvcTest {

    @Autowired
    private MockMvc mockMvc;

    @MockitoBean
    private BranchService branchService;

    @MockitoBean
    private AsyncUseCase asyncUseCase;

    @Autowired
    private ObjectMapper objectMapper;

    // ===============================
    // CREATE BRANCH
    // ===============================

    @Test
    @DisplayName("POST /api/branches - should create branch")
    void createBranch_shouldReturnCreated() throws Exception {

        BranchDTO dto = new BranchDTO();
        dto.setBranchCode("BR001");
        dto.setName("Main Branch");

        Branch saved = new Branch();
        saved.setId(1L);
        saved.setBranchCode("BR001");
        saved.setName("Main Branch");

        when(branchService.createBranch(any(BranchDTO.class)))
                .thenReturn(saved);

        mockMvc.perform(post("/api/branches")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(dto)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(1L))
                .andExpect(jsonPath("$.branchCode").value("BR001"))
                .andExpect(jsonPath("$.name").value("Main Branch"));

        verify(branchService).createBranch(any(BranchDTO.class));
    }

    // ===============================
    // GET BY BRANCH CODE
    // ===============================

    @Test
    void getBranchByBranchCode_shouldReturnBranch() throws Exception {

        Branch branch = new Branch();
        branch.setId(1L);
        branch.setBranchCode("BR001");
        branch.setName("Main Branch");

        when(branchService.getBranchByBranchCode("BR001"))
                .thenReturn(branch);

        mockMvc.perform(get("/api/branches/BR001"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.branchCode").value("BR001"))
                .andExpect(jsonPath("$.name").value("Main Branch"));

        verify(branchService).getBranchByBranchCode("BR001");
    }

    // ===============================
    // GET ALL
    // ===============================

    @Test
    void getAllBranches_shouldReturnList() throws Exception {

        Branch branch1 = new Branch(1L,"BR001", "BR001", "Main");
        Branch branch2 = new Branch(2L, "BR002","BR002", "City");

        when(branchService.getAllBranches())
                .thenReturn(List.of(branch1, branch2));

        mockMvc.perform(get("/api/branches"))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.size()", is(2)))
                .andExpect(jsonPath("$[0].branchCode").value("BR001"))
                .andExpect(jsonPath("$[1].branchCode").value("BR002"));

        verify(branchService).getAllBranches();
    }

    // ===============================
    // UPDATE
    // ===============================

    @Test
    void updateBranch_shouldReturnUpdated() throws Exception {

        BranchDTO dto = new BranchDTO();
        dto.setBranchCode("BR001");
        dto.setName("Updated Branch");

        Branch updated = new Branch(1L, "Updated Branch", "BR001","CTR");

        when(branchService.updateBranch(eq(1L), any(BranchDTO.class)))
                .thenReturn(updated);

        mockMvc.perform(put("/api/branches/1")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(dto)))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.name").value("Updated Branch"));

        verify(branchService).updateBranch(eq(1L), any(BranchDTO.class));
    }

    // ===============================
    // DELETE
    // ===============================

    @Test
    void deleteBranch_shouldReturnNoContent() throws Exception {

        mockMvc.perform(delete("/api/branches/1"))
                .andExpect(status().isNoContent());

        verify(branchService).deleteBranch(1L);
    }

    // ===============================
    // EXISTS
    // ===============================

    @Test
    void isBranchExists_shouldReturnTrue() throws Exception {

        when(branchService.isBranchExists("BR001"))
                .thenReturn(true);

        mockMvc.perform(get("/api/branches/exists/BR001"))
                .andExpect(status().isOk())
                .andExpect(content().string("true"));

        verify(branchService).isBranchExists("BR001");
    }

    // ===============================
    // ASYNC ENDPOINT
    // ===============================

    @Test
    void asyncEndpoint_shouldReturnOk() throws Exception {

        mockMvc.perform(get("/api/branches/async"))
                .andExpect(status().isOk())
                .andExpect(content().string("OK"));

        verify(asyncUseCase).sendEmail(anyString());
    }

    // ===============================
    // VALIDATION FAILURE
    // ===============================

    @Test
    void createBranch_invalidInput_shouldReturnBadRequest() throws Exception {

        BranchDTO dto = new BranchDTO(); // empty → should fail validation

        mockMvc.perform(post("/api/branches")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(dto)))
                .andExpect(status().isBadRequest());
    }

}

This site uses Just the Docs, a documentation theme for Jekyll.