Spring Boot Hello World Part 2

post-thumb

Java is still one of the most widely used programming languages in the world, and Spring Boot is the de-facto framework for building production-ready services with it. Surprisingly, even though I’ve worked with Java before, I had never actually touched Spring Boot.

That changes today.

In this series, we’ll build a simple CRUD API for tasks step by step.

In Part 1 we built a simple CRUD API for tasks using Spring Boot. In this part, we’ll add fast, isolated web tests for the controller using @WebMvcTest and MockMvc. These tests spin up only the Spring MVC slice (no full application context or server), so they’re quick and focused.

👉 Source for this part: Git repository (part 2 branch).

👉 Parts:

  • Part 1: Basics — install the tools and build a simple REST API.
  • Part 2 (this post): Add unit tests for our endpoints.
  • Part 3: Store tasks in a real NoSQL database (Redis).

Testing architecture

Scope / StyleWhat It LoadsWhat It SkipsSpeedUse ForTypical AnnotationsMocking Needs
MVC sliceSpring MVC layer: @Controller/@RestController, @ControllerAdvice, Jackson, validation, message convertersFull auto-config, embedded server, security (unless added), data layer, scheduling, messagingFastController I/O, request/response shapes, status codes, JSON payloads@WebMvcTest(YourController.class), MockMvcYes (via @MockBean) for any collaborators
Web + full contextWhole application context; auto-config; optional embedded server with webEnvironment— (loads everything—can be heavy)Medium–SlowEnd-to-end integration, filters/interceptors, security, full wiring@SpringBootTest, @AutoConfigureMockMvc or TestRestTemplateUsually no (real beans), or mock external deps
Standalone MockMvcOnly the controller under test, wired manuallySpring context (no DI), all auto-configFastestUltra-focused unit tests on controller logic without SpringMockMvcBuilders.standaloneSetup(new YourController())Manual stubs (you construct dependencies)
Data sliceJPA repositories, entity mapping, transaction mgmtWeb layerFastRepository queries, entity mapping@DataJpaTestNot typical
JSON serializationJackson ObjectMapperEverything elseFastDTO <-> JSON correctness, custom serializers/deserializers@JsonTestNot typical

When to choose what?

  • Use @WebMvcTest (what we do here) when you want HTTP behavior without booting the entire app.
  • Use @SpringBootTest + @AutoConfigureMockMvc when you need filters, security, or full wiring.
  • Use standalone MockMvc for pure unit tests where Spring’s dependency injection isn’t needed.
  • Use @DataJpaTest for repository queries and database mapping.
  • Use @JsonTest for serialization-only scenarios.

Handy snippets

Mock a collaborator in @WebMvcTest:

@WebMvcTest(TaskController.class)
class TaskControllerTests {

  @Autowired MockMvc mockMvc;

  @MockBean TaskService taskService; // if your controller calls a service

  // tests...
}

Full context with MockMvc (integration-style):

@SpringBootTest
@AutoConfigureMockMvc
class HttpIntegrationTests {

  @Autowired MockMvc mockMvc;

  // tests...
}

Standalone MockMvc (no Spring context):

import static org.springframework.test.web.servlet.setup.MockMvcBuilders.standaloneSetup;

class StandaloneControllerTests {

  MockMvc mockMvc = standaloneSetup(new TaskController(/* deps */)).build();

  // tests...
}

What we’ll test

  • Creating a task (POST /tasks)
  • Listing tasks (GET /tasks)
  • Fetching a single task (GET /tasks/{id})
  • Updating a task (PUT /tasks/{id})
  • Deleting a task (DELETE /tasks/{id})
  • Proper 404s for non-existing IDs

Dependencies

Add test dependencies to build.gradle:

dependencies {
  testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

test {
  useJUnitPlatform()
}

Controller tests with MockMvc

Create src/test/java/fi/janihast/springboot_hello_world/TaskControllerTests.java:

package fi.janihast.springboot_hello_world;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import fi.janihast.springboot_hello_world.controllers.TaskController;
import java.util.UUID;
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.web.servlet.MockMvc;
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.*;

@WebMvcTest(TaskController.class)
class TaskControllerTests {

  @Autowired
  private MockMvc mockMvc;

  // Helper method to create a task and return its ID
  private String createTaskAndGetId(String taskName) throws Exception {
    String taskJson = "{\"name\":\"" + taskName + "\"}";

    final String[] taskId = new String[1]; // Array to make it effectively final

    // Create task and read ID
    mockMvc.perform(post("/tasks")
      .contentType(MediaType.APPLICATION_JSON)
      .content(taskJson))
      .andExpect(status().isOk())
      .andDo(result -> {
        String content = result.getResponse().getContentAsString();
        ObjectMapper mapper = new ObjectMapper();
        JsonNode json = mapper.readTree(content);
        taskId[0] = json.get("task").get("id").asText();
      });

    return taskId[0];
  }


  @Test
  void shouldCreateTaskAndRetrieveList() throws Exception {
    String taskId = createTaskAndGetId("Task to be retrieved");

    // Retrieve the list of tasks and check if the created task is present
    this.mockMvc.perform(get("/tasks"))
      .andExpect(status().isOk())
      .andExpect(content().contentType(MediaType.APPLICATION_JSON))
      .andExpect(jsonPath("$.tasks").exists())
      .andExpect(jsonPath("$.tasks[?(@.id=='" + taskId + "')].name").exists());
  }

  @Test 
  void shouldCreateTaskAndRetrieveIt() throws Exception {
    String taskId = createTaskAndGetId("Task to be retrieved");

    // Retrieve the task by ID and check its id and name
    mockMvc.perform(get("/tasks/" + taskId))
      .andExpect(status().isOk())
      .andExpect(jsonPath("$.task.id").value(taskId))
      .andExpect(jsonPath("$.task.name").value("Task to be retrieved"));
  }

  @Test
  void shouldCreateTask() throws Exception {
    String taskJson = "{\"name\":\"Test Task\"}";

    // Create the task and verify response
    mockMvc.perform(post("/tasks")
      .contentType(MediaType.APPLICATION_JSON)
      .content(taskJson))
      .andExpect(status().isOk())
      .andExpect(content().contentType(MediaType.APPLICATION_JSON))
      .andExpect(jsonPath("$.task").exists())
      .andExpect(jsonPath("$.task.name").value("Test Task"))
      .andExpect(jsonPath("$.task.id").exists());
  }

  @Test
  void shouldCreateTaskAndDeleteIt() throws Exception {
    String taskId = createTaskAndGetId("Task to be deleted");

    // Verify the task exists
    mockMvc.perform(get("/tasks/" + taskId))
      .andExpect(status().isOk());

    // Delete the task
    mockMvc.perform(delete("/tasks/" + taskId))
      .andExpect(status().isOk());

    // Verify the task no longer exists
    mockMvc.perform(get("/tasks/" + taskId))
      .andExpect(status().isNotFound());
  }

  @Test
  void shouldUpdateTask() throws Exception {
    String taskId = createTaskAndGetId("Task to be updated");

    String updatedTaskJson = "{\"name\":\"Updated Task Name\"}";

    // Update the task
    mockMvc.perform(put("/tasks/" + taskId)
      .contentType(MediaType.APPLICATION_JSON)
      .content(updatedTaskJson))
      .andExpect(status().isOk())
      .andExpect(jsonPath("$.task.id").value(taskId))
      .andExpect(jsonPath("$.task.name").value("Updated Task Name"));

    // Retrieve the task and verify the update
    mockMvc.perform(get("/tasks/" + taskId))
      .andExpect(status().isOk())
      .andExpect(jsonPath("$.task.id").value(taskId))
      .andExpect(jsonPath("$.task.name").value("Updated Task Name"));
  }

  @Test
  void shouldReturn404ForNonExistentTaskOnRetrieve() throws Exception {
    UUID randomId = UUID.randomUUID();

    // Attempt to retrieve a non-existent task
    mockMvc.perform(get("/tasks/" + randomId))
      .andExpect(status().isNotFound());
  }

  @Test
  void shouldReturn404ForNonExistentTaskOnUpdate() throws Exception {
    UUID randomId = UUID.randomUUID();

    String updatedTaskJson = "{\"name\":\"Updated Task Name\"}";

    // Attempt to update a non-existent task
    mockMvc.perform(put("/tasks/" + randomId)
      .contentType(MediaType.APPLICATION_JSON)
      .content(updatedTaskJson))
      .andExpect(status().isNotFound());
  }

  @Test
  void shouldReturn404ForNonExistentTaskOnDelete() throws Exception {
    UUID randomId = UUID.randomUUID();

    // Attempt to delete a non-existent task
    mockMvc.perform(delete("/tasks/" + randomId))
      .andExpect(status().isNotFound());
  }

}

Why @WebMvcTest?

  • Loads only MVC infrastructure (controllers, Jackson, validation, etc.)
  • Doesn’t start the full app or embedded server → fast
  • Perfect for controller I/O behavior and HTTP status checks

If your controller depends on services or repositories, you’d add @MockBean for those. Our controller is self-contained, so no mocks are needed here.

Recap

At this point, we have a fully working test suite:

Test nameEndpointExpected outcome
shouldCreateTaskPOST /tasks200 + JSON with id & name
shouldCreateTaskAndRetrieveListGET /tasksList contains created task
shouldCreateTaskAndRetrieveItGET /tasks/{id}Returns correct id + name
shouldUpdateTaskPUT /tasks/{id}Returns updated name
shouldCreateTaskAndDeleteItDELETE /tasks/{id}200 + then GET returns 404
shouldReturn404ForNonExistentTaskGET/PUT/DELETE/{id}Returns 404

In the next part, we’ll add persistent storage for our tasks.

References

Quick reference

Here’s a quick cheatsheet for running your tests:

Run the whole test suite

gradle test

Run single test class

gradle test --tests "fi.janihast.springboot_hello_world.TaskControllerTests"

Run single test method

gradle test --tests "fi.janihast.springboot_hello_world.TaskControllerTests.shouldCreateTaskAndRetrieveList"
comments powered by Disqus