Controller Testing

  1. Controller testing checks that REST endpoints return the right status codes and JSON without starting a server or opening a browser.
  2. It is used to verify URL mapping, request-body conversion and response handling quickly and automatically.
  3. Spring’s MockMvc sends fake HTTP requests straight into the controller and lets you assert on the response.
  4. Only the web layer is loaded, and the service is mocked, so no MySQL is needed.

Features

  • Test GET, POST, PUT and DELETE endpoints without Postman or a browser.
  • Assert HTTP status codes (200, 201, 404, 409).
  • Assert JSON response fields with jsonPath.
  • Send JSON request bodies using ObjectMapper.
  • Load only the controller layer with @WebMvcTest, which makes tests fast.
  • Mock the service with @MockBean to isolate the controller.

Hands-on Experiment: Creating a spring boot application to understandhow Spring MVC endpoints can be tested without manually invoking them through a browser.

The following steps are involved in developing this hands-on experiment.

Step 1: Generate the Project

Step 2: Project Structure

Step 3: pom.xml

Step 4: application.properties

Step 5: Create Entity Class

Step 6: Create Repository Interface

Step 7: Create Service Class

Step 8: Create Controller Class

Step 9: Main Application Class

Step 10: Run the Main Application Class

Step 1: Configure the Project on Spring Initializr

Go to start.spring.io and set:

  • Project: Maven
  • Language: Java
  • Spring Boot version: 3.2.x
  • Group: com.example
  • Artifact: mockmvcdemo
  • Name: mockmvcdemo
  • Package name: com.example.mockmvcdemo
  • Packaging: Jar
  • Java: 17
  • Dependencies: Spring Web, Spring Data JPA, MySQL Driver.

Click Generate to download the ZIP, then extract and import it into your IDE as a Maven project.

Step 2: Generated Project Structure

Step 3: Explore pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.2.5</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>student-mockmvc-demo</artifactId>
    <version>1.0.0</version>
    <name>student-mockmvc-demo</name>
    <description>Testing Student REST endpoints using MockMvc</description>

    <properties>
        <java.version>17</java.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- Includes JUnit 5, Mockito, MockMvc and JsonPath -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>

Step 4: application.properties

spring.application.name=student-mockmvc-demo
server.port=8082

# MySQL (database is created automatically if it does not exist) - change username/password as needed
spring.datasource.url=jdbc:mysql://localhost:3306/studentdb?createDatabaseIfNotExist=true&useSSL=false&serverTimezone=UTC&allowPublicKeyRetrieval=true
spring.datasource.username=root
spring.datasource.password=password

spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=trueCode language: PHP (php)

Step 5: Create Entity Class

package com.example.mockmvcdemo.entity;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import jakarta.persistence.Table;

@Entity
@Table(name = "students")
public class Student {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;
    private String email;
    private String department;

    public Student() {
    }

    public Student(Long id, String name, String email, String department) {
        this.id = id;
        this.name = name;
        this.email = email;
        this.department = department;
    }

    public Long getId() { return id; }
    public void setId(Long id) { this.id = id; }

    public String getName() { return name; }
    public void setName(String name) { this.name = name; }

    public String getEmail() { return email; }
    public void setEmail(String email) { this.email = email; }

    public String getDepartment() { return department; }
    public void setDepartment(String department) { this.department = department; }
}

Step 6: Create below Repository Interface

package com.example.mockmvcdemo.repository;

import com.example.mockmvcdemo.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.Optional;

@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {

    Optional<Student> findByEmail(String email);
}

Step 7: Create below Service class

package com.example.mockmvcdemo.service;

import com.example.mockmvcdemo.entity.Student;
import com.example.mockmvcdemo.repository.StudentRepository;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Service;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;

@Service
public class StudentService {

    private final StudentRepository studentRepository;

    public StudentService(StudentRepository studentRepository) {
        this.studentRepository = studentRepository;
    }

    public Student addStudent(Student student) {
        if (studentRepository.findByEmail(student.getEmail()).isPresent()) {
            throw new ResponseStatusException(HttpStatus.CONFLICT,
                    "Email already registered: " + student.getEmail());
        }
        return studentRepository.save(student);
    }

    public List<Student> getAllStudents() {
        return studentRepository.findAll();
    }

    public Student getStudentById(Long id) {
        return studentRepository.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
                        "Student not found with id: " + id));
    }

    public void deleteStudent(Long id) {
        if (!studentRepository.existsById(id)) {
            throw new ResponseStatusException(HttpStatus.NOT_FOUND,
                    "Student not found with id: " + id);
        }
        studentRepository.deleteById(id);
    }
}

Step 8: Create Controller class

package com.example.mockmvcdemo.controller;

import com.example.mockmvcdemo.entity.Student;
import com.example.mockmvcdemo.service.StudentService;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/students")
public class StudentController {

    private final StudentService studentService;

    public StudentController(StudentService studentService) {
        this.studentService = studentService;
    }

    @PostMapping
    @ResponseStatus(HttpStatus.CREATED)
    public Student addStudent(@RequestBody Student student) {
        return studentService.addStudent(student);
    }

    @GetMapping
    public List<Student> getAllStudents() {
        return studentService.getAllStudents();
    }

    @GetMapping("/{id}")
    public Student getStudentById(@PathVariable Long id) {
        return studentService.getStudentById(id);
    }

    @DeleteMapping("/{id}")
    public String deleteStudent(@PathVariable Long id) {
        studentService.deleteStudent(id);
        return "Student deleted with id: " + id;
    }
}

Step 9: Main Application

package com.example.mockmvcdemo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class MockMvcDemoApplication {

    public static void main(String[] args) {
        SpringApplication.run(MockMvcDemoApplication.class, args);
    }
}
package com.example.mockmvcdemo.controller;

import com.example.mockmvcdemo.entity.Student;
import com.example.mockmvcdemo.service.StudentService;
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.boot.test.mock.mockito.MockBean;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.web.server.ResponseStatusException;

import java.util.List;

import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

// Loads only the web layer (controller + MVC). No database, no server, no browser.
@WebMvcTest(StudentController.class)
class StudentControllerTest {

    @Autowired
    private MockMvc mockMvc;                 // sends fake HTTP requests to the controller

    @Autowired
    private ObjectMapper objectMapper;       // converts Student <-> JSON

    @MockBean
    private StudentService studentService;   // controller's dependency is mocked

    @Test
    @DisplayName("POST /students returns 201 and the saved student")
    void addStudent() throws Exception {
        Student request = new Student(null, "Ravi", "ravi@mail.com", "CSE");
        Student saved = new Student(1L, "Ravi", "ravi@mail.com", "CSE");

        when(studentService.addStudent(any(Student.class))).thenReturn(saved);

        mockMvc.perform(post("/students")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.name").value("Ravi"))
                .andExpect(jsonPath("$.email").value("ravi@mail.com"))
                .andExpect(jsonPath("$.department").value("CSE"));
    }

    @Test
    @DisplayName("POST /students returns 409 when the email already exists")
    void addStudent_duplicate() throws Exception {
        Student request = new Student(null, "Ravi", "ravi@mail.com", "CSE");

        when(studentService.addStudent(any(Student.class)))
                .thenThrow(new ResponseStatusException(HttpStatus.CONFLICT, "Email already registered"));

        mockMvc.perform(post("/students")
                        .contentType(MediaType.APPLICATION_JSON)
                        .content(objectMapper.writeValueAsString(request)))
                .andExpect(status().isConflict());
    }

    @Test
    @DisplayName("GET /students returns the list of students")
    void getAllStudents() throws Exception {
        when(studentService.getAllStudents()).thenReturn(List.of(
                new Student(1L, "Ravi", "ravi@mail.com", "CSE"),
                new Student(2L, "Sita", "sita@mail.com", "ECE")));

        mockMvc.perform(get("/students"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.length()").value(2))
                .andExpect(jsonPath("$[0].name").value("Ravi"))
                .andExpect(jsonPath("$[1].name").value("Sita"));
    }

    @Test
    @DisplayName("GET /students/{id} returns the student when found")
    void getStudentById() throws Exception {
        when(studentService.getStudentById(1L))
                .thenReturn(new Student(1L, "Ravi", "ravi@mail.com", "CSE"));

        mockMvc.perform(get("/students/{id}", 1L))
                .andExpect(status().isOk())
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.name").value("Ravi"));
    }

    @Test
    @DisplayName("GET /students/{id} returns 404 when not found")
    void getStudentById_notFound() throws Exception {
        when(studentService.getStudentById(99L))
                .thenThrow(new ResponseStatusException(HttpStatus.NOT_FOUND, "Student not found"));

        mockMvc.perform(get("/students/{id}", 99L))
                .andExpect(status().isNotFound());
    }

    @Test
    @DisplayName("DELETE /students/{id} returns 200 and a confirmation message")
    void deleteStudent() throws Exception {
        mockMvc.perform(delete("/students/{id}", 1L))
                .andExpect(status().isOk())
                .andExpect(content().string("Student deleted with id: 1"));

        verify(studentService).deleteStudent(1L);
    }

    @Test
    @DisplayName("DELETE /students/{id} returns 404 when student is missing")
    void deleteStudent_notFound() throws Exception {
        org.mockito.Mockito.doThrow(new ResponseStatusException(HttpStatus.NOT_FOUND, "Student not found"))
                .when(studentService).deleteStudent(99L);

        mockMvc.perform(delete("/students/{id}", 99L))
                .andExpect(status().isNotFound());
    }
}

Step 10: Run the StudentControllerTest

Scroll to Top