# 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`:

```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**.

```java
@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 `postService`It 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)**

```java
@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):**

```java
@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

```java
@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

```java
@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

```java
@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 value**s.

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

```java
@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](https://github.com/mdjc/blog-posts-app/commit/5078c1c48cd6035f4d6548cd082e31ce7f645a8a) and dive deeper into MockMvc [here](https://docs.spring.io/spring-framework/reference/6.1/testing/spring-mvc-test-framework.html).

Happy testing! 🚀
