Testing your REST APIs in Spring Boot

Testing REST APIs in Spring Boot ensures your endpoints work as expected before they reach production. In this guide, I'll walk you through writing unit tests for a Spring Boot REST API using MockMvc. We'll cover setting up dependencies, writing test cases for different endpoints, understanding Spring’s testing behavior, and useful tips to improve your tests.

1. Setting Up Dependencies

Before we start writing tests, we need to add the necessary dependencies in pom.xml:

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

What Does spring-boot-starter-test Provide?

The spring-boot-starter-test dependency is an all-in-one test suite for Spring Boot applications. It includes:

JUnit 5 – The default testing framework for writing test cases.
Mockito – A library for mocking dependencies in unit tests.
Spring Test – Provides utilities like MockMvc to test Spring MVC controllers without starting a real server.
AssertJ & Hamcrest – Libraries for writing fluent assertions.
JsonPath – A utility for extracting and asserting JSON responses.

This starter eliminates the need to configure multiple testing dependencies manually, making Spring Boot testing seamless.


2. Setting Up Your Test Class

Now that we have the dependencies, let's set up a test class for our PostController. This class will allow us to simulate HTTP requests and validate the responses without starting a real Spring Boot application.

@WebMvcTest(PostController.class)
@MockitoBeans({ @MockitoBean(types = PostService.class) })
class PostControllerTest {

  @Autowired
  private MockMvc mockMvc;

  @Autowired
  private PostService postService;

  @InjectMocks
  private PostController controller;

  private final ObjectMapper objectMapper = new ObjectMapper();
}

Breaking It Down

🔹 @WebMvcTest(PostController.class)

  • This annotation loads only the web layer (controller, filters, and related components).

  • It ensures faster test execution by excluding unnecessary beans such as repositories or services.

🔹 @MockitoBeans and @MockitoBean(PostService.class)

  • @MockitoBeans is the newest approach (Spring Boot 3.1+) for defining mocks in tests.

  • It replaces the older @MockBean approach by grouping mock definitions in a structured way.

  • Why do we need this?

    • Since @WebMvcTest does not load service beans, we mock the PostService to avoid calling the real service or database.

🔹 @Autowired MockMvc

  • MockMvc is injected into our test class to simulate HTTP requests to our controller.

  • It allows us to: ✅ Perform GET, POST, PUT, and DELETE requests.
    ✅ Validate HTTP status codes and response content.
    ✅ Test controllers without starting a real server.

🔹 @Autowired PostService

  • Since we're using @MockitoBean(PostService.class), Spring automatically injects the mocked version of PostService into this field.

  • Why do we need this?

    • This ensures our test class does not depend on real service logic but still allows us to verify interactions.

🔹 @InjectMocks PostController

  • This tells Mockito to inject mocked dependencies (like postService) into our PostController instance.

  • Why do we need this?

    • Spring usually handles dependency injection in a real app, but in a test environment, we need to inject mocks into the controller manually.

    • This ensures that when PostController calls postServiceIt uses the mocked version, not the real implementation.

🔹 ObjectMapper

  • ObjectMapper is used to convert Java objects to JSON and vice versa.

  • Why do we need this?

    • When making POST or PUT requests, we need to send JSON request bodies.

    • When verifying responses, we may deserialize JSON into Java objects for assertions.

3. Writing Tests for Each Endpoint

GET /api/posts – Fetch All Posts

Basic Example (Testing Response Size Only)

@Test
void testGetAllPosts_basic() throws Exception {
    List<PostDto> posts = List.of(
        new PostDto(1L, "Title 1", "Content 1"),
        new PostDto(2L, "Title 2", "Content 2"));

    when(postService.getAllPosts()).thenReturn(posts);

    mockMvc.perform(get("/api/posts"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.size()").value(posts.size()));
}

Mocks the service to return a list of posts.
✅ Uses jsonPath("$.size()") to verify the response size.

Extended Example (Validating Response Content):

@Test
void testGetAllPosts_extended() throws Exception {
    List<PostDto> posts =
        List.of(new PostDto(1L, "Title 1", "Content example 1", fixedCreatedAt),
            new PostDto(2L, "Title 2", "Content example 2", fixedCreatedAt),
            new PostDto(3L, "Title 3", "Content example 3", fixedCreatedAt));

    when(postService.getAllPosts()).thenReturn(posts);

    MvcResult mvcResult = mockMvc
        .perform(MockMvcRequestBuilders.get("/api/posts").contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.size()").value(posts.size())).andReturn();

    JsonNode jsonResponse = objectMapper.readTree(mvcResult.getResponse().getContentAsString());

    IntStream.range(0, posts.size()).forEach(i -> {
      try {
        PostDto expectedPost = posts.get(i);
        JsonNode actualPost = jsonResponse.get(i);

        assertEquals(expectedPost.id(), actualPost.get("id").asLong());
        assertEquals(expectedPost.title(), actualPost.get("title").asText());
        assertEquals(expectedPost.content(), actualPost.get("content").asText());
        assertEquals(expectedPost.createdAt().format(formatter), actualPost.get("createdAt").asText());

      } catch (Exception e) {
        Assertions.fail("Unexpected exception: " + e.getMessage());
      }
    });
}

✅ Iterates through each post and validates ID, title, content, and timestamp.

✅ The use of IntStream.range(0, posts.size()) ensures that you iterate over the response in the exact order of the expected list (posts).

GET /api/posts/{id} – Fetch a Single Post

@Test
void testGetPostById_extended() throws Exception {
    PostDto post = new PostDto(1L, "Test Post", "Test Content", fixedCreatedAt);

    when(postService.getPostById(1L)).thenReturn(post);

    mockMvc.perform(get("/api/posts/1"))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.id").value(post.id()))
        .andExpect(jsonPath("$.title").value(post.title()))
        .andExpect(jsonPath("$.content").value(post.content()))
        .andExpect(jsonPath("$.createdAt").value(post.createdAt().format(formatter)));
}

✅ Ensures title, content, and createdAt fields are correctly returned.

Edge Case: Post Not Found

@Test
void testGetPostById_notFound() throws Exception {
    when(postService.getPostById(99L))
        .thenThrow(new ResourceNotFoundException("Post with ID 99 not found"));

    mockMvc.perform(MockMvcRequestBuilders.get("/api/posts/{id}", 99L)
            .contentType(MediaType.APPLICATION_JSON))
        .andExpect(status().isNotFound())
        .andExpect(result -> assertTrue(result.getResolvedException() instanceof ResourceNotFoundException))
        .andExpect(result -> assertEquals("Post with ID 99 not found",
            result.getResolvedException().getMessage()));
}

Simulates the scenario where a post does not exist by making the service throw ResourceNotFoundException.
Ensures the response is properly handled by checking for HTTP 404 Not Found.
Verifies the correct exception is thrown in the controller (ResourceNotFoundException).
Checks the error message to ensure it matches what is expected.
Applicable to multiple endpoints, including GET, PUT, and DELETE, ensuring robustness.

PUT /api/posts/{id} – Update an Existing Post

@Test
void testUpdatePost_success() throws Exception {
    UpdatePostDto updatePost = new UpdatePostDto("Updated Title", "Updated Content");
    PostDto updatedPost = new PostDto(1L, "Updated Title", "Updated Content");

    when(postService.updatePost(eq(1L), any(UpdatePostDto.class))).thenReturn(updatedPost);

    mockMvc.perform(put("/api/posts/1")
        .contentType(MediaType.APPLICATION_JSON)
        .content(objectMapper.writeValueAsString(updatePost)))
        .andExpect(status().isOk())
        .andExpect(jsonPath("$.title").value("Updated Title"))
        .andExpect(jsonPath("$.content").value("Updated Content"));
}

✅ Ensures that the updated title and content match the expected values.

DELETE /api/posts/{id} – Delete a Post

@Test
void testDeletePost_successful() throws Exception {
    Long postId = 1L;

    mockMvc.perform(delete("/api/posts/{id}", postId))
        .andExpect(status().isNoContent());

    verify(postService, times(1)).delete(postId);
}

✅ Uses verify() to check if postService.delete() was called once.

4. How Spring Boot Executes These Tests Internally

Spring Boot leverages MockMvc (The Spring MVC Test framework) to simulate HTTP requests without starting a real server. t does that by invoking the DispatcherServlet and passing “mock” implementations of the Servlet API from the spring-test module which replicates the full Spring MVC request handling without a running server.

Key points:

  1. These tests are not integration or end-to-end tests. It’s not a classic unit test but they are a little closer to it

  2. Tests the server-side, so you can check what handler was used, if an exception was handled with a HandlerExceptionResolver, what the content of the model is, what binding errors there were, and other details. That means that it is easier to write expectations, since the server is not an opaque box, as it is when testing it through an actual HTTP client.

  3. @WebMvcTest loads only the web layer (controller + filters).


5. Quick tips for Writing Better Tests

Test Edge Cases – Ensure tests cover not found scenarios, validation failures, and bad requests. We’ve demonstrated how to test for not-found scenarios but we can always go further as the API handles validations, additional business logic, or becomes more complex.
Use Meaningful Assertions – Check more than just HTTP status; validate response content.
Isolate Tests – Use mocks to avoid database interactions.
Keep Tests Independent – Each test should not depend on other tests' execution.


Conclusion

Writing tests for Spring Boot REST APIs helps us ensure reliability. By using MockMvc, we can simulate requests, validate responses, and handle edge cases efficiently.

You can check out a real implementation in my GitHub repo: View on GitHub and dive deeper into MockMvc here.

Happy testing! 🚀

0
Subscribe to my newsletter

Read articles from Mirna De Jesus Cambero directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Mirna De Jesus Cambero
Mirna De Jesus Cambero

I’m a backend software engineer with over a decade of experience primarily in Java. I started this blog to share what I’ve learned in a simplified, approachable way — and to add value for fellow developers. Though I’m an introvert, I’ve chosen to put myself out there to encourage more women to explore and thrive in tech. I believe that by sharing what we know, we learn twice as much — that’s precisely why I’m here.