Integration Testing

  1. Integration testing checks that several application layers work correctly together as one workflow.
  2. 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.
  3. Here a request goes through Controller, Service and Repository into a real MySQL test database, and nothing is mocked.
  4. @SpringBootTest loads the full application context, and MockMvc sends 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

Scroll to Top