Spring Boot CRUD REST APIs Integration Testing Example

In this tutorial, we will learn how to write great integration tests for our Spring Boot CRUD Rest APIs. 

We will first develop Spring Boot CRUD REST APIs using Spring Boot, Spring Data JPA( Hibernate), and MySQL database and then we will write integration tests for these CRUD REST APIs using TestRestTemplate class.

@SpringBootTest Annotation

Spring Boot provides @SpringBootTest annotation for Integration testing. This annotation creates an application context and loads the full application context.

@SpringBootTest will bootstrap the full application context, which means we can @Autowire any bean that's picked up by component scanning into our test.
It starts the embedded server, creates a web environment, and then enables @Test methods to do integration testing.

By default, @SpringBootTest  does not start a server. We need to add the attribute webEnvironment to further refine how your tests run. It has several options: 
  • MOCK(Default): Loads a web ApplicationContext and provides a mock web environment.
  • RANDOM_PORT: Loads a WebServerApplicationContext and provides a real web environment. The embedded server is started and listened to a random port. This is the one that should be used for the integration test.
  • DEFINED_PORT: Loads a WebServerApplicationContext and provides a real web environment. NONE: Loads an ApplicationContext by using SpringApplication but does not provide any web environment.

So basically, the @SpringBootTest annotation tells Spring Boot to look for the main configuration class (one with @SpringBootApplication, for instance) and use that to start a Spring application context.

Important: A key part of integration testing is testing all the layers in the application.

1. What we’ll build

We are building a simple User Management Application which has below CRUD Rest APIs. We are going to build the following five REST APIs (Controller handler methods) for the User resource.
Spring Boot 2 Hibernate 5 MySQL CRUD REST API Tutorial

1. Creating Spring Boot Project

There are many ways to create a Spring Boot application. The simplest way is to use Spring Initializr at http://start.spring.io/, which is an online Spring Boot application generator.
 
Use the following details while creating the Spring Boot project:
  • Generate: Maven Project
  • Java Version: 17 (Default)
  • Spring Boot:3.0.4
  • Group: com.companyname
  • Artifact: springbootcrudrest
  • Name: springbootcrudrest
  • Description: Rest API for a Simple User Management Application
  • Package Name : com.companyname.springbootcrudrest
  • Packaging: jar (This is the default value)
  • Dependencies: Web, JPA, MySQL, DevTools
Once, all the details are entered, next, click on Generate Project button will generate a spring boot project and downloads it. Next, Unzip the downloaded zip file and import it into your favorite IDE.

2. Packaging Structure

Following is the packing structure of our User Management Application -

    3. Maven Dependencies - 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 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    	<modelVersion>4.0.0</modelVersion>
    
    	<groupId>com.companyname</groupId>
    	<artifactId>springbootcrudrest</artifactId>
    	<version>0.0.1-SNAPSHOT</version>
    	<packaging>jar</packaging>
    
    	<name>springbootcrudrest</name>
    	<description>Demo project for Spring Boot</description>
    
    	<parent>
    		<groupId>org.springframework.boot</groupId>
    		<artifactId>spring-boot-starter-parent</artifactId>
    		<version>3.0.4</version>
    		<relativePath/> <!-- lookup parent from repository -->
    	</parent>
    
    	<properties>
    		<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    		<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
    		<java.version>17</java.version>
    	</properties>
    
    	<dependencies>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-data-jpa</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-web</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-starter-validation</artifactId>
    		</dependency>
    		<dependency>
    			<groupId>org.springframework.boot</groupId>
    			<artifactId>spring-boot-devtools</artifactId>
    			<scope>runtime</scope>
    		</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>
    From the above pom.xml, let's understand a few important spring boot features.

    Spring Boot Maven plugin

    The Spring Boot Maven plugin provides many convenient features:
    • It collects all the jars on the classpath and builds a single, runnable "├╝ber-jar", which makes it more convenient to execute and transport your service.
    • It searches for the public static void main() method to flag as a runnable class.
    • It provides a built-in dependency resolver that sets the version number to match Spring Boot dependencies. You can override any version you wish, but it will default to Boot’s chosen set of versions.

    spring-boot-starter-parent

    All Spring Boot projects typically use spring-boot-starter-parent as the parent in pom.xml.
        <parent>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-parent</artifactId>
            <version>3.0.4</version>
        </parent>
    The spring-boot-starter-parent allows you to manage the following things for multiple child projects and modules:
    • Configuration - Java Version and Other Properties
    • Dependency Management - Version of dependencies
    • Default Plugin Configuration

    spring-boot-starter-web

    We added the spring-boot-starter-web dependency, which will by default pulls all the commonly used libraries while developing Spring MVC applications, such as spring-webmvc, jackson-json, validation-api, and Tomcat.

    spring-boot-starter-data-jpa

    We added the spring-boot-starter-data-jpa dependency. This pulls all the spring-data-jpa dependencies and adds Hibernate libraries because most applications use Hibernate as a JPA implementation.

    3. Configuring MySQL Database

    Since we’re using MySQL as our database, we need to configure the database URL, username, and password so that Spring can establish a connection with the database on startup. 

    Open the src/main/resources/application.properties file and add the following properties to it -
    spring.datasource.url = jdbc:mysql://localhost:3306/users_database?useSSL=false
    spring.datasource.username = root
    spring.datasource.password = root
    
    
    ## Hibernate Properties
    # The SQL dialect makes Hibernate generate better SQL for the chosen database
    spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQLDialect
    
    # Hibernate ddl auto (create, create-drop, validate, update)
    spring.jpa.hibernate.ddl-auto = update
    Don’t forget to change the spring.datasource.username and spring.datasource.password as per your MySQL installation. Also, create a database named users_database in MySQL before proceeding to the next section.

    You don’t need to create any tables. The tables will automatically be created by hibernate from the User entity that we will define in the next step. This is made possible by the property spring.jpa.hibernate.ddl-auto = update.

    4. Create a JPA Entity - User

    Let's create a User model or domain class with the following fields:
    • id - primary key
    • firstName - user's first name
    • lastName - user last name
    • emailId - user email ID
    • createdAt - user object created date
    • createdBy - use an object created by
    • updatedAt - user object updated by
    • updatedby - user object updated by
    package com.companyname.springbootcrudrest.model;
    
    import java.util.Date;
    
    import jakarta.persistence.*;
    
    import org.springframework.data.annotation.CreatedBy;
    import org.springframework.data.annotation.CreatedDate;
    import org.springframework.data.annotation.LastModifiedBy;
    import org.springframework.data.annotation.LastModifiedDate;
    import org.springframework.data.jpa.domain.support.AuditingEntityListener;
    
    @Entity
    @Table(name = "users")
    @EntityListeners(AuditingEntityListener.class)
    public class User {
    
        private long id;
        private String firstName;
        private String lastName;
        private String emailId;
        private Date createdAt;
        private String createdBy;
        private Date updatedAt;
        private String updatedby;
     
        @Id
        @GeneratedValue(strategy = GenerationType.AUTO)
        public long getId() {
            return id;
        }
        public void setId(long id) {
            this.id = id;
        }
     
        @Column(name = "first_name", nullable = false)
        public String getFirstName() {
            return firstName;
        }
        public void setFirstName(String firstName) {
            this.firstName = firstName;
        }
     
        @Column(name = "last_name", nullable = false)
        public String getLastName() {
            return lastName;
        }
        public void setLastName(String lastName) {
            this.lastName = lastName;
        }
     
        @Column(name = "email_address", nullable = false)
        public String getEmailId() {
            return emailId;
        }
        public void setEmailId(String emailId) {
            this.emailId = emailId;
        }
     
        @Column(name = "created_at", nullable = false)
        @CreatedDate
        public Date getCreatedAt() {
            return createdAt;
        }
        public void setCreatedAt(Date createdAt) {
           this.createdAt = createdAt;
        }
     
        @Column(name = "created_by", nullable = false)
        @CreatedBy
        public String getCreatedBy() {
            return createdBy;
        }
        public void setCreatedBy(String createdBy) {
            this.createdBy = createdBy;
        }
     
        @Column(name = "updated_at", nullable = false)
        @LastModifiedDate
        public Date getUpdatedAt() {
            return updatedAt;
        }
        public void setUpdatedAt(Date updatedAt) {
            this.updatedAt = updatedAt;
        }
     
        @Column(name = "updated_by", nullable = false)
        @LastModifiedBy
        public String getUpdatedby() {
            return updatedby;
        }
        public void setUpdatedby(String updatedby) {
           this.updatedby = updatedby;
        }
    }

    All your domain models must be annotated with @Entity annotation. It is used to mark the class as a persistent Java class.

    @Table annotation is used to provide the details of the table that this entity will be mapped to.

    @Id annotation is used to define the primary key.

    @GeneratedValue annotation is used to define the primary key generation strategy. In the above case, we have declared the primary key to be an Auto Increment field.

    @Column annotation is used to define the properties of the column that will be mapped to the annotated field. You can define several properties like name, length, nullable, updateable etc.

    How to enable JPA Auditing

    Let's understand important JPA Auditing annotations:
    • @CreatedDate - Declares a field as the one representing the date the entity containing the field was created.
    • @LastModifiedDate - Declares a field as the one representing the date the entity containing the field was recently modified.
    • @CreatedBy- Declares a field as the one representing the principal that created the entity containing the field.
    • @LastModifiedBy - Declares a field as the one representing the principal that recently modified the entity containing the field.
    Now, what we want is that these fields should automatically get populated whenever we create or update an entity. 

    To achieve this, we need to do two things -
    1. Add Spring Data JPA’s AuditingEntityListener to the domain model. We have already done this in our User model with the annotation @EntityListeners(AuditingEntityListener.class).
    @Entity
    @Table(name = "users")
    @EntityListeners(AuditingEntityListener.class)
    public class User {
     // rest of the code here
    }
    2. Enable JPA Auditing in the main application.
    Open SpringBootCrudRestApplication.java and add a @EnableJpaAuditing annotation.
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
    
    @SpringBootApplication
    @EnableJpaAuditing
    public class SpringBootCrudRestApplication {
        public static void main(String[] args) {
            SpringApplication.run(SpringBootCrudRestApplication.class, args);
        }
    }
    So far we have created a User model and enabled JPA auditing on it. Next, we need to create a repository to access user records to and from a database.

    5. Create Spring Data JPA Repository - UserRepository

    import org.springframework.data.jpa.repository.JpaRepository;
    
    import com.companyname.springbootcrudrest.model.User;
    
    public interface UserRepository extends JpaRepository<User, Long>{
    
    }
    In the above code, the UserRepository interface extends JpaRepository which provides the below methods to deal with database operations:
    List<T> findAll();
    List<T> findAll(Sort sort);
    List<T> findAllById(Iterable<ID> ids);
    <S extends T> List<S> saveAll(Iterable<S> entities);
    void flush();
    <S extends T> S saveAndFlush(S entity);
    void deleteInBatch(Iterable<T> entities);
    void deleteAllInBatch();
    T getOne(ID id);
    @Override
    <S extends T> List<S> findAll(Example<S> example);
    <S extends T> List<S> findAll(Example<S> example, Sort sort);

    6. Exception(Error) Handling for RESTful Services

    ResourceNotFoundException

    Let's create a ResourceNotFoundException class with the following content in it:
    package com.companyname.springbootcrudrest.exception;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.web.bind.annotation.ResponseStatus;
    
    @ResponseStatus(value = HttpStatus.NOT_FOUND)
    public class ResourceNotFoundException extends Exception{
    
        private static final long serialVersionUID = 1L;
    
        public ResourceNotFoundException(String message){
            super(message);
        }
    }

    ErrorDetails

    Let's create ErrorDetails that we will use to customize the error response structure:
    package com.companyname.springbootcrudrest.exception;
    
    import java.util.Date;
    
    public class ErrorDetails {
        private Date timestamp;
        private String message;
        private String details;
    
        public ErrorDetails(Date timestamp, String message, String details) {
            super();
            this.timestamp = timestamp;
            this.message = message;
            this.details = details;
        }
    
        public Date getTimestamp() {
            return timestamp;
        }
    
        public String getMessage() {
            return message;
        }
    
        public String getDetails() {
            return details;
        }
    }

    GlobalExceptionHandler

    To use ErrorDetails to return the error response, let’s create a GlobalExceptionHandler class annotated with @ControllerAdvice annotation. This class handles exception-specific and global exceptions in a single place.
    package com.companyname.springbootcrudrest.exception;
    
    import java.util.Date;
    
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.ControllerAdvice;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.context.request.WebRequest;
    
    @ControllerAdvice
    public class GlobalExceptionHandler {
        @ExceptionHandler(ResourceNotFoundException.class)
        public ResponseEntity<?> resourceNotFoundException(ResourceNotFoundException ex, WebRequest request) {
        ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
            return new ResponseEntity<>(errorDetails, HttpStatus.NOT_FOUND);
        }
    
        @ExceptionHandler(Exception.class)
        public ResponseEntity<?> globleExcpetionHandler(Exception ex, WebRequest request) {
            ErrorDetails errorDetails = new ErrorDetails(new Date(), ex.getMessage(), request.getDescription(false));
            return new ResponseEntity<>(errorDetails, HttpStatus.INTERNAL_SERVER_ERROR);
        }
    }

    7. Creating UserController(Contains REST APIs)

    Now, it's time to create CRUD Rest APIs for the User model.
    package com.companyname.springbootcrudrest.controller;
    
    import java.util.Date;
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    import jakarta.validation.Valid;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.DeleteMapping;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.PutMapping;
    import org.springframework.web.bind.annotation.RequestBody;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    import com.companyname.springbootcrudrest.exception.ResourceNotFoundException;
    import com.companyname.springbootcrudrest.model.User;
    import com.companyname.springbootcrudrest.repository.UserRepository;
    
    @RestController
    @RequestMapping("/api/v1")
    public class UserController {
     
        @Autowired
        private UserRepository userRepository;
    
        @GetMapping("/users")
        public List<User> getAllUsers() {
            return userRepository.findAll();
        }
    
        @GetMapping("/users/{id}")
        public ResponseEntity<User> getUserById(
        @PathVariable(value = "id") Long userId) throws ResourceNotFoundException {
            User user = userRepository.findById(userId)
            .orElseThrow(() -> new ResourceNotFoundException("User not found on :: "+ userId));
            return ResponseEntity.ok().body(user);
        }
    
        @PostMapping("/users")
        public User createUser(@Valid @RequestBody User user) {
            return userRepository.save(user);
        }
    
        @PutMapping("/users/{id}")
        public ResponseEntity<User> updateUser(
        @PathVariable(value = "id") Long userId,
        @Valid @RequestBody User userDetails) throws ResourceNotFoundException {
             User user = userRepository.findById(userId)
              .orElseThrow(() -> new ResourceNotFoundException("User not found on :: "+ userId));
      
            user.setEmailId(userDetails.getEmailId());
            user.setLastName(userDetails.getLastName());
            user.setFirstName(userDetails.getFirstName());
            user.setUpdatedAt(new Date());
            final User updatedUser = userRepository.save(user);
            return ResponseEntity.ok(updatedUser);
       }
    
       @DeleteMapping("/user/{id}")
       public Map<String, Boolean> deleteUser(
           @PathVariable(value = "id") Long userId) throws Exception {
           User user = userRepository.findById(userId)
              .orElseThrow(() -> new ResourceNotFoundException("User not found on :: "+ userId));
    
           userRepository.delete(user);
           Map<String, Boolean> response = new HashMap<>();
           response.put("deleted", Boolean.TRUE);
           return response;
       }
    }
    Let's understand all the annotations used in the UserController:

    @RequestMapping("/api/v1") - annotation declares that the URL for all the APIs in this controller will start with /api/v1

    @RestController - annotation is a combination of Spring’s @Controller and @ResponseBody annotations.

    @GetMapping - annotation is a short form of @RequestMapping(method=RequestMethod.GET). This annotation is used to map incoming HTTP GET requests to a specific method handler.

    @PostMapping - annotation is a short form of @RequestMapping(method=RequestMethod.POST). This annotation is used to map incoming HTTP POST requests to a specific method handler.

    @PutMapping - annotation is a short form of @RequestMapping(method=RequestMethod.PUT).This annotation is used to map incoming HTTP PUT requests to a specific method handler.

    @DeleteMapping - annotation is a short form of @RequestMapping(method=RequestMethod.DELETE). This annotation is used to map incoming HTTP DELETE requests to a specific method handler.

    @PathVariable - annotation is used to bind a path variable with a method parameter.

    8. Running the Application

    We have successfully developed all the CRUD Rest APIs for the User model. now it's time to deploy our application in a servlet container(embedded tomcat). Two ways we can start the standalone Spring boot application. 

    1. From the root directory of the application and type the following command to run it -
    $ mvn spring-boot:run
    2. From your IDE, run the SpringBootCrudRestApplication.main() method as a standalone Java class that will start the embedded Tomcat server on port 8080 and point the browser to http://localhost:8080/.

        11. Integration Testing CRUD REST APIs

        Let's write the Integration tests for CRUD REST APIs. We are going to use TestRestTemplate class to call the REST APIs (GET, POST, PUT and DELETE):
        package com.companyname.projectname.springbootcrudrest;
        
        import static org.junit.Assert.assertEquals;
        import static org.junit.Assert.assertNotNull;
        
        import org.junit.Test;
        import org.junit.runner.RunWith;
        import org.springframework.beans.factory.annotation.Autowired;
        import org.springframework.boot.test.context.SpringBootTest;
        import org.springframework.boot.test.web.client.TestRestTemplate;
        import org.springframework.boot.web.server.LocalServerPort;
        import org.springframework.http.HttpEntity;
        import org.springframework.http.HttpHeaders;
        import org.springframework.http.HttpMethod;
        import org.springframework.http.HttpStatus;
        import org.springframework.http.ResponseEntity;
        import org.springframework.test.context.junit4.SpringRunner;
        import org.springframework.web.client.HttpClientErrorException;
        
        import com.companyname.springbootcrudrest.SpringBootCrudRestApplication;
        import com.companyname.springbootcrudrest.model.User;
        
        @RunWith(SpringRunner.class)
        @SpringBootTest(classes = SpringBootCrudRestApplication.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
        public class SpringBootCrudRestApplicationTests {
        
            @Autowired
            private TestRestTemplate restTemplate;
        
            @LocalServerPort
            private int port;
        
            private String getRootUrl() {
                return "http://localhost:" + port;
            }
        
            @Test
            public void contextLoads() {
        
            }
        
            @Test
            public void testGetAllUsers() {
                 HttpHeaders headers = new HttpHeaders();
                 HttpEntity<String> entity = new HttpEntity<String>(null, headers);
        
                 ResponseEntity<String> response = restTemplate.exchange(getRootUrl() + "/users",
                 HttpMethod.GET, entity, String.class);
          
                 assertNotNull(response.getBody());
            }
        
            @Test
            public void testGetUserById() {
                User user = restTemplate.getForObject(getRootUrl() + "/users/1", User.class);
                System.out.println(user.getFirstName());
                assertNotNull(user);
            }
        
            @Test
            public void testCreateUser() {
                User user = new User();
                user.setEmailId("admin@gmail.com");
                user.setFirstName("admin");
                user.setLastName("admin");
                user.setCreatedBy("admin");
                user.setUpdatedby("admin");
        
                ResponseEntity<User> postResponse = restTemplate.postForEntity(getRootUrl() + "/users", user, User.class);
                assertNotNull(postResponse);
                assertNotNull(postResponse.getBody());
            }
        
            @Test
            public void testUpdatePost() {
                 int id = 1;
                 User user = restTemplate.getForObject(getRootUrl() + "/users/" + id, User.class);
                 user.setFirstName("admin1");
                 user.setLastName("admin2");
        
                 restTemplate.put(getRootUrl() + "/users/" + id, user);
        
                 User updatedUser = restTemplate.getForObject(getRootUrl() + "/users/" + id, User.class);
                 assertNotNull(updatedUser);
            }
        
            @Test
            public void testDeletePost() {
                 int id = 2;
                 User user = restTemplate.getForObject(getRootUrl() + "/users/" + id, User.class);
                 assertNotNull(user);
        
                 restTemplate.delete(getRootUrl() + "/users/" + id);
            
                 try {
                      user = restTemplate.getForObject(getRootUrl() + "/users/" + id, User.class);
                 } catch (final HttpClientErrorException e) {
                 assertEquals(e.getStatusCode(), HttpStatus.NOT_FOUND);
             }
          }
        
        }

        Key points about integration tests

        • While running the integration tests that start the embedded servlet containers, it is better to use WebEnvironment.RANDOM_PORT so that it won’t conflict with other running applications, especially in Continuous Integration (CI) environments where multiple builds run in parallel.
        • You can specify which configuration classes to use to build ApplicationContext by using the classes attribute of the @SpringBootTest annotation.
        @SpringBootTest(classes = Application.class, webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
        • The TestRestTemplate bean will be registered automatically only when @SpringBootTest is started with an embedded servlet container.
        • As you need to test the REST endpoint, you start the embedded servlet container by specifying the webEnvironment attribute of @SpringBootTest.

        Output


        From the above Spring Boot application development let's list out the advantages of using Spring boot.
        • Simpler dependency management
        • Default auto-configuration
        • Embedded web server
        • Application metrics and health checks
        • Advanced externalized configuration

        Source Code on GitHub

        The source code of this article available on my GitHub repository at EmployeeControllerIntegrationTest.java 
        Learn Spring Boot on Spring Boot Tutorial

        Free Spring Boot Tutorial | Full In-depth Course | Learn Spring Boot in 10 Hours


        Watch this course on YouTube at Spring Boot Tutorial | Fee 10 Hours Full Course