- Integration testing checks that several application layers work correctly together as one workflow.
- It is used to catch problems that unit tests miss, such as wrong wiring, wrong JSON mapping or a query that doesn’t match the service logic.
- Here a request goes through Controller, Service and Repository into a real MySQL test database, and nothing is mocked.
@SpringBootTestloads the full application context, andMockMvcsends the requests.
Features
- Tests the complete flow: Controller to Service to Repository to database.
- Loads the full Spring context, so the real beans are wired together.
- Uses a separate MySQL test database (
studentdb_test), so real data is safe. - Verifies the real database state after each API call.
- Tests complete workflows such as create, read, update, delete.
- Needs no running server, no port and no Postman.
Hands-on Experiment: Creating a spring boot application to understand how multiple Spring Boot application layers can be tested together as one workflow.
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: integrationdemo
- Name: integrationdemo
- Package name: com.example.integrationdemo
- 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-integration-demo</artifactId>
<version>1.0.0</version>
<name>student-integration-demo</name>
<description>Integration testing the Student API from Controller to Service to Repository</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>
<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-integration-demo
server.port=8082
# Main (real) database - 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.integrationdemo.entity;
import jakarta.persistence.Column;
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;
@Column(unique = true)
private String email;
private String department;
public Student() {
}
public Student(String name, String email, String department) {
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.integrationdemo.repository;
import com.example.integrationdemo.entity.Student;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
public interface StudentRepository extends JpaRepository<Student, Long> {
boolean existsByEmail(String email);
List<Student> findByDepartment(String department);
List<Student> findByNameContainingIgnoreCase(String keyword);
}
Step 7: Create below Service class
package com.example.integrationdemo.service;
import com.example.integrationdemo.entity.Student;
import com.example.integrationdemo.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.existsByEmail(student.getEmail())) {
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 List<Student> getByDepartment(String department) {
return studentRepository.findByDepartment(department);
}
public List<Student> searchByName(String keyword) {
return studentRepository.findByNameContainingIgnoreCase(keyword);
}
public Student updateStudent(Long id, Student updated) {
Student existing = getStudentById(id);
existing.setName(updated.getName());
existing.setDepartment(updated.getDepartment());
return studentRepository.save(existing);
}
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.integrationdemo.controller;
import com.example.integrationdemo.entity.Student;
import com.example.integrationdemo.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);
}
@GetMapping("/department/{department}")
public List<Student> getByDepartment(@PathVariable String department) {
return studentService.getByDepartment(department);
}
@GetMapping("/search")
public List<Student> search(@RequestParam String name) {
return studentService.searchByName(name);
}
@PutMapping("/{id}")
public Student updateStudent(@PathVariable Long id, @RequestBody Student student) {
return studentService.updateStudent(id, student);
}
@DeleteMapping("/{id}")
public String deleteStudent(@PathVariable Long id) {
studentService.deleteStudent(id);
return "Student deleted with id: " + id;
}
}
Step 9: Main Application
package com.example.integrationdemo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class IntegrationDemoApplication {
public static void main(String[] args) {
SpringApplication.run(IntegrationDemoApplication.class, args);
}
}
package com.example.integrationdemo.controller;
import com.example.integrationdemo.entity.Student;
import com.example.integrationdemo.repository.StudentRepository;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.jupiter.api.BeforeEach;
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.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.http.MediaType;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;
import static org.junit.jupiter.api.Assertions.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.*;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;
// Full integration test: NOTHING is mocked.
// Request -> StudentController -> StudentService -> StudentRepository -> MySQL test database.
// @SpringBootTest loads the complete application context; MockMvc sends the requests.
@SpringBootTest
@AutoConfigureMockMvc
@ActiveProfiles("test")
class StudentApiIntegrationTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ObjectMapper objectMapper;
@Autowired
private StudentRepository studentRepository; // used only to check what really reached the database
@BeforeEach
void cleanDatabase() {
studentRepository.deleteAll();
}
private MvcResult createStudent(String name, String email, String department) throws Exception {
Student request = new Student(name, email, department);
return mockMvc.perform(post("/students")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(request)))
.andExpect(status().isCreated())
.andReturn();
}
private long idFrom(MvcResult result) throws Exception {
return objectMapper.readTree(result.getResponse().getContentAsString()).get("id").asLong();
}
@Test
@DisplayName("POST /students saves the student in the database")
void createStudent_savedInDatabase() throws Exception {
MvcResult result = createStudent("Ravi Kumar", "ravi@mail.com", "CSE");
long id = idFrom(result);
assertTrue(studentRepository.existsById(id));
assertEquals("ravi@mail.com", studentRepository.findById(id).orElseThrow().getEmail());
}
@Test
@DisplayName("POST /students with an existing email returns 409 and saves nothing new")
void createStudent_duplicateEmail() throws Exception {
createStudent("Ravi Kumar", "ravi@mail.com", "CSE");
mockMvc.perform(post("/students")
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new Student("Another", "ravi@mail.com", "IT"))))
.andExpect(status().isConflict());
assertEquals(1, studentRepository.count());
}
@Test
@DisplayName("GET /students/{id} returns a student saved earlier")
void getStudentById() throws Exception {
long id = idFrom(createStudent("Sita Devi", "sita@mail.com", "ECE"));
mockMvc.perform(get("/students/{id}", id))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Sita Devi"))
.andExpect(jsonPath("$.department").value("ECE"));
}
@Test
@DisplayName("GET /students/{id} returns 404 for an unknown id")
void getStudentById_notFound() throws Exception {
mockMvc.perform(get("/students/{id}", 99999L))
.andExpect(status().isNotFound());
}
@Test
@DisplayName("GET /students returns all saved students")
void getAllStudents() throws Exception {
createStudent("Ravi Kumar", "ravi@mail.com", "CSE");
createStudent("Sita Devi", "sita@mail.com", "ECE");
mockMvc.perform(get("/students"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(2));
}
@Test
@DisplayName("GET /students/department/{department} filters by department")
void getByDepartment() throws Exception {
createStudent("Ravi Kumar", "ravi@mail.com", "CSE");
createStudent("Anil Sharma", "anil@mail.com", "CSE");
createStudent("Sita Devi", "sita@mail.com", "ECE");
mockMvc.perform(get("/students/department/{department}", "CSE"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(2));
}
@Test
@DisplayName("GET /students/search?name= searches by part of the name")
void searchByName() throws Exception {
createStudent("Ravi Kumar", "ravi@mail.com", "CSE");
createStudent("Sita Devi", "sita@mail.com", "ECE");
mockMvc.perform(get("/students/search").param("name", "KUMAR"))
.andExpect(status().isOk())
.andExpect(jsonPath("$.length()").value(1))
.andExpect(jsonPath("$[0].name").value("Ravi Kumar"));
}
@Test
@DisplayName("Complete workflow: create -> read -> update -> delete -> read again (404)")
void completeWorkflow() throws Exception {
// 1. create
long id = idFrom(createStudent("Mohan", "mohan@mail.com", "IT"));
// 2. read
mockMvc.perform(get("/students/{id}", id))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Mohan"));
// 3. update
mockMvc.perform(put("/students/{id}", id)
.contentType(MediaType.APPLICATION_JSON)
.content(objectMapper.writeValueAsString(new Student("Mohan Reddy", "mohan@mail.com", "CSE"))))
.andExpect(status().isOk())
.andExpect(jsonPath("$.name").value("Mohan Reddy"))
.andExpect(jsonPath("$.department").value("CSE"));
assertEquals("Mohan Reddy", studentRepository.findById(id).orElseThrow().getName());
// 4. delete
mockMvc.perform(delete("/students/{id}", id))
.andExpect(status().isOk())
.andExpect(content().string("Student deleted with id: " + id));
assertFalse(studentRepository.existsById(id));
// 5. read again -> not found
mockMvc.perform(get("/students/{id}", id))
.andExpect(status().isNotFound());
}
}
Step 10: Run the StudentApiInegrationTest

