Test a Spring Boot REST Controller with JUnit and Mockito

Test a Spring Boot REST controller with JUnit and Mockito. Compare direct method tests, standalone MockMvc and @WebMvcTest using verified GET, POST and 404 examples.

Spring rest controller unit test example

A direct Mockito test checks a controller method as Java code; a MockMvc test also checks its Spring MVC route and HTTP response. Both can use JUnit and a mocked service, but they answer different questions. Choose the smaller test when you need only the controller’s decision, and send a request through MVC when status codes, JSON or headers matter.

This example uses Spring Boot 4.1.2. The same delivery controller is tested three ways: a direct call, standalone MockMvc, and a Boot @WebMvcTest slice. Each step adds only the Spring infrastructure needed for the next assertion.

1. Choose the Test You Need

A controller that receives a service can be constructed directly with a Mockito mock. That test does not load Spring or make an HTTP request. Standalone MockMvc creates the controller directly but routes a mock HTTP request through Spring MVC. @WebMvcTest asks Spring Boot to load an MVC test slice, including the selected controller and its web configuration, while @MockitoBean supplies the service it needs.

Test setup Spring application context What the assertion can check
JUnit with @Mock and @InjectMocks None Java method result and service calls
MockMvcBuilders.standaloneSetup(controller) None MVC mapping, JSON, status and headers for configured controller
@WebMvcTest(DeliveryController.class) MVC slice The same HTTP behavior with Boot’s MVC configuration

The project uses the Boot 4 MVC starter for the controller and its matching test starter for JUnit, Mockito, AssertJ and Spring MVC test support. The Boot parent manages their versions.

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webmvc</artifactId>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-webmvc-test</artifactId>
  <scope>test</scope>
</dependency>

The Spring Boot testing reference describes what the MVC slice loads. A full MockMvc tutorial covers more request methods and validation cases; here the focus is choosing the boundary of a controller test.

2. Define the Delivery Controller

A delivery application lets a customer look up a parcel by tracking ID. It can also create a delivery request from an address. DeliveryController receives a DeliveryService in its constructor. The service returns an Optional for a lookup and a DeliveryStatus for a new request.

public record DeliveryStatus(String trackingId, String state) {}

public interface DeliveryService {
  Optional<DeliveryStatus> find(String trackingId);
  DeliveryStatus create(String address);
}

For a found tracking ID, ResponseEntity.of() returns 200 with the status. If the service returns Optional.empty(), Spring returns 404. The POST method returns 201 and a Location header containing the new tracking ID. These outcomes give the tests concrete behavior to check.

@RestController
@RequestMapping("/deliveries")
public class DeliveryController {
  private final DeliveryService service;

  public DeliveryController(DeliveryService service) { this.service = service; }

  @GetMapping("/{trackingId}")
  public ResponseEntity<DeliveryStatus> find(@PathVariable String trackingId) {
    return ResponseEntity.of(service.find(trackingId));
  }

  @PostMapping
  public ResponseEntity<DeliveryStatus> create(@RequestBody NewDelivery request) {
    DeliveryStatus saved = service.create(request.address());
    return ResponseEntity.created(URI.create("/deliveries/" + saved.trackingId()))
        .body(saved);
  }

  public record NewDelivery(String address) {}
}

The test project does not implement a real delivery service because these tests target the controller’s response rules. The mock supplies the service result for each case. A separate service test would check real lookup and creation logic.

3. Call the Controller Directly with Mockito

@ExtendWith(MockitoExtension.class) creates a mock for the @Mock field and injects it into the controller marked @InjectMocks. No Spring context or servlet request exists. The test stubs the service, calls find() directly, and checks the returned ResponseEntity and service argument.

@ExtendWith(MockitoExtension.class)
class DeliveryControllerDirectTest {
  @Mock DeliveryService service;
  @InjectMocks DeliveryController controller;

  @Test
  void callsServiceAndReturnsItsStatus() {
    DeliveryStatus status = new DeliveryStatus("D-17", "OUT_FOR_DELIVERY");
    when(service.find("D-17")).thenReturn(Optional.of(status));

    var response = controller.find("D-17");

    assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
    assertThat(response.getBody()).isEqualTo(status);
    verify(service).find("D-17");
  }
}

The test proves that find() passes D-17 to the service and returns the supplied status in an HTTP 200 response object. It does not prove that /deliveries/D-17 maps to the method or that the record becomes the expected JSON. Those steps require an MVC request test.

The same direct setup checks the controller’s empty-result branch without an HTTP request:

when(service.find("missing")).thenReturn(Optional.empty());
assertThat(controller.find("missing").getStatusCode())
    .isEqualTo(HttpStatus.NOT_FOUND);

This is a useful fast check of the method’s decision, but it still does not exercise Spring MVC’s routing or response writing.

4. Send a Mock HTTP Request Without Boot

MockMvcBuilders.standaloneSetup() registers the controller with Spring MVC test infrastructure. The test constructs the controller with a Mockito mock, so it still does not start the full application context. Unlike the direct call, mvc.perform() sends a mock GET request through the route and serializes the response body.

@BeforeEach
void setUp() {
  service = mock(DeliveryService.class);
  mvc = MockMvcBuilders.standaloneSetup(new DeliveryController(service)).build();
}

@Test
void getSerializesTheStatusAsJson() throws Exception {
  when(service.find("D-17"))
      .thenReturn(Optional.of(new DeliveryStatus("D-17", "OUT_FOR_DELIVERY")));
  mvc.perform(get("/deliveries/D-17"))
      .andExpect(status().isOk())
      .andExpect(jsonPath("$.trackingId").value("D-17"))
      .andExpect(jsonPath("$.state").value("OUT_FOR_DELIVERY"));
}

Now a wrong @GetMapping path or JSON field name would fail the test. Standalone setup uses only the MVC components registered for the test; it does not automatically include every converter, filter or controller advice configured by the application. Use a Boot slice when that configuration matters.

The POST test also checks the response header that a direct method test might overlook. It sends JSON with an address, stubs the created delivery, and asserts 201, Location and the JSON tracking ID.

when(service.create("12 Main Street"))
    .thenReturn(new DeliveryStatus("D-18", "CREATED"));
mvc.perform(post("/deliveries").contentType(MediaType.APPLICATION_JSON)
        .content("{\"address\":\"12 Main Street\"}"))
    .andExpect(status().isCreated())
    .andExpect(header().string("Location", "/deliveries/D-18"))
    .andExpect(jsonPath("$.trackingId").value("D-18"));

The POST body reaches the controller’s @RequestBody parameter as a NewDelivery record. The mocked service returns D-18; the controller builds the Location header from that ID. The test checks the visible HTTP result rather than assuming the header was set.

5. Load Boot’s MVC Test Slice

@WebMvcTest(DeliveryController.class) loads the selected controller and Boot’s MVC test configuration. It does not load ordinary service beans, so @MockitoBean adds a mock DeliveryService to the test context. Boot also configures MockMvc for the slice.

@WebMvcTest(DeliveryController.class)
class DeliveryControllerSliceTest {
  @Autowired MockMvc mvc;
  @MockitoBean DeliveryService service;

  @Test
  void testsBootConfiguredMvcResponse() throws Exception {
    when(service.find("D-17"))
        .thenReturn(Optional.of(new DeliveryStatus("D-17", "OUT_FOR_DELIVERY")));
    mvc.perform(get("/deliveries/D-17"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.state").value("OUT_FOR_DELIVERY"));
  }
}

The response is still HTTP 200 with OUT_FOR_DELIVERY, but this time Spring Boot created the controller and MVC components. The service result is simulated, so the test checks controller routing, serialization and wiring, not delivery lookup logic. In Spring Boot 4, use Spring Framework’s @MockitoBean in place of the removed Boot @MockBean annotation.

The slice also tests the missing status through an HTTP request:

when(service.find("missing")).thenReturn(Optional.empty());
mvc.perform(get("/deliveries/missing"))
    .andExpect(status().isNotFound());

The test is useful when the API contract requires 404 for an unknown tracking ID. It would catch a change to the controller that returned 200 with an empty body.

6. Frequently Asked Questions

6.1. Is a Direct Controller Call an HTTP Test?

No. Calling controller.find(“D-17”) executes a Java method. It does not invoke Spring MVC’s route matching, request conversion, filters or response serialization. Use MockMvc when the HTTP contract is what needs checking.

6.2. Does Standalone MockMvc Load the Application Context?

No. standaloneSetup() builds MVC test infrastructure around the controllers and other components you supply. It can be useful for a narrow HTTP test, but add relevant advice or converters explicitly when the test depends on them.

6.3. Why Does @WebMvcTest Need @MockitoBean?

The MVC slice loads the selected controller but excludes the normal service layer. The controller still needs a DeliveryService constructor argument. @MockitoBean supplies that bean and lets the test control its response. For a test that should use the real service and repository, use @SpringBootTest as shown in the integration-testing guide.

7. Conclusion

Call the controller directly for its Java decision logic. Use standalone MockMvc to check a small HTTP route without Boot configuration, or @WebMvcTest when Boot’s MVC configuration is part of the behavior. Keep the mocked service result separate from claims about real service or database behavior.

Happy Learning!

Complete REST controller test example on GitHub

Leave a Comment

  1. with this test how can it work when you use MvcMock without Mocking Repository
    @Test
    public void getAllEmployeesAPI() throws Exception
    {
    mvc.perform(MockMvcRequestBuilders
    .get(“/employees”)
    .accept(MediaType.APPLICATION_JSON))
    .andDo(print())
    .andExpect(status().isOk())
    .andExpect(MockMvcResultMatchers.jsonPath(“$.employees”).exists())
    .andExpect(MockMvcResultMatchers.jsonPath(“$.employees[*].employeeId”).isNotEmpty());
    }

Comments are closed.

About Us

HowToDoInJava provides tutorials and how-to guides on Java and related technologies.

It also shares the best practices, algorithms & solutions and frequently asked interview questions.