Writing Unit Tests for REST APIs with Springboot

Spring Boot provides utilities and annotations to facilitate testing. This can be achieved by using tools like Mockito and JUnit to mock dependencies.

· 8 min read
Victor Ogonyo

Victor Ogonyo

Backend and DevOps Engineer

In this tutorial, we are going to implement unit testing in a springboot REST API project. We are going to implement unit testing on an Employee Management System with the basic Create, Read, Update and Delete Functionality.

Unit Testing Introduction

Types of Testing

1) End To End Testing - Testing the entire application, its external dependencies in a production like environment

2) Functional Testing - Testing an application from a users point of view

3) Integration Testing - Ensuring different modules of an application work together correctly.

4) Unit testing - Testing individual components of an application for example methods or classes in isolation. For this tutorial we are going to focus on this.

Building the REST API

Step 1: Setup the Project

To set up the project use a spring initializer. Go to https://start.spring.io.Add the following dependencies to your project: Spring Web, Spring Data JPA, H2 Database, SpringBoot Dev Tools, Springboot Test. Also for this project we are going to use Java 17 , maven and Jar for packaging.

Step 2: Create the Employee Model

Create an Employee Class with the following fields: id, firstName, lastName, Department and email. Here is the Employee Class with the Getters and Setters

@Entity
@Table(name = "employees")
public class Employee {

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

    @Column(nullable = false)
    private String firstName;

    @Column(nullable = false)
    private String lastName;

    @Column(nullable = false, unique = true)
    private String email;

    @Column(nullable = false)
    private String department;

    // Default constructor
    public Employee() {
    }

    public Employee(long id, String firstName, String lastName, String email, String department) {
        this.id = id;
        this.firstName = firstName;
        this.lastName = lastName;
        this.email = email;
        this.department = department;
    }

    // getters and setters

    public Long getId() {
        return id;
    }

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

    public String getFirstName() {
        return firstName;
    }

    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }

    public String getLastName() {
        return lastName;
    }

    public void setLastName(String lastName) {
        this.lastName = lastName;
    }

    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 3: Create the Employee Respository

@Repository
public interface EmployeeRepository extends JpaRepository<Employee, Long> {
      Boolean existsByEmail(String email);
}

Step 4: Create the Employee Service


@Service
public class EmployeeService {

    @Autowired
    private EmployeeRepository employeeRepository;

    public Employee createEmployee(Employee employee) {
        return employeeRepository.save(employee);
    }

    public Employee updateEmployee(Long id, Employee updatedEmployee) {
        Employee existingEmployee = employeeRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Employee not found"));
        existingEmployee.setFirstName(updatedEmployee.getFirstName());
        existingEmployee.setLastName(updatedEmployee.getLastName());
        existingEmployee.setEmail(updatedEmployee.getEmail());
        existingEmployee.setDepartment(updatedEmployee.getDepartment());
        return employeeRepository.save(existingEmployee);
    }

    public void deleteEmployee(Long id) {
        Employee employee = employeeRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Employee not found"));
        employeeRepository.delete(employee);
    }

    public List<Employee> getAllEmployees() {
        return employeeRepository.findAll();
    }

    public Employee getEmployeeById(Long id) {
        return employeeRepository.findById(id).orElseThrow(() -> new ResourceNotFoundException("Employee not found"));
    }
}

Step 5: Create the Employee Controller

@RestController
@RequestMapping("/api/employees")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;

    @PostMapping
    public ResponseEntity<Employee> createEmployee(@RequestBody Employee employee) {
        Employee createdEmployee = employeeService.createEmployee(employee);
        return new ResponseEntity<>(createdEmployee, HttpStatus.CREATED);
    }

    @PutMapping("/{id}")
    public ResponseEntity<Employee> updateEmployee(@PathVariable Long id, @RequestBody Employee employee) {
        Employee updatedEmployee = employeeService.updateEmployee(id, employee);
        return new ResponseEntity<>(updatedEmployee, HttpStatus.OK);
    }

    @DeleteMapping("/{id}")
    public ResponseEntity<Void> deleteEmployee(@PathVariable Long id) {
        employeeService.deleteEmployee(id);
        return new ResponseEntity<>(HttpStatus.NO_CONTENT);
    }

    @GetMapping
    public ResponseEntity<List<Employee>> getAllEmployees() {
        List<Employee> employees = employeeService.getAllEmployees();
        return new ResponseEntity<>(employees, HttpStatus.OK);
    }

    @GetMapping("/{id}")
    public ResponseEntity<Employee> getEmployeeById(@PathVariable Long id) {
        Employee employee = employeeService.getEmployeeById(id);
        return new ResponseEntity<>(employee, HttpStatus.OK);
    }
}

Step 6: Create the ResourceNotFoundException Class

public class ResourceNotFoundException extends RuntimeException {
    public ResourceNotFoundException(String message) {
        super(message);
    }
}

This is how the folder Structure looks like:

Image

Feel Free to Access the code at this repository: https://github.com/kodaschool/unit-testing-springboot

Testing Dependencies

Each project generated by the springboot has testing in mind. The Package below has all the libraries required to do unit testing. This dependency comes by default when generating a springboot project. Check the pom.xml file

.

    <dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>

Inspect the dependencies it comes bundled with by hitting ctrl + Click on windows or cmd + click on mac on the artifactId.

Some of the depedencies include:

1) JUnit - De- facto standard for unit testing in java

2) Spring Test and Springboot Test - Utilities and integration test support for Springboot applications

3) AssertJ - An assertion library

4) Hamcrest - A library of matcher objects

5) Mockito - A java mocking framework

6) JsonAssert - An assertion library for JSON

7) JsonPath - Xpath for JSON

Unit Testing Annotations

@SpringBootTest

  • It is used to load the application context and run tests with Spring Boot features.
  • It can be used as an alternative to the @ContextConfiguration annotation when you need Spring Boot features.
  • It creates the application context used in tests through SpringApplication.
  • It allows you to test the integration of different components and services.
  • It can be configured to load specific classes, set the environment, and set properties.
  • It provides support for different web environment modes, including the ability to start a fully running web server

@DataJpaTest

  • @DataJpaTest is used to test JPA components in Spring Boot.
  • It disables full auto-configuration and applies only configuration relevant to JPA tests.
  • By default, it uses an embedded in-memory database for testing.
  • The application context contains all JPA components, including the in-memory database.
  • Each test method runs in its own transaction and is rolled back after execution.
  • Ensure you use this annotation when testing the repository layer.
  • Useful for testing JPA repositories and custom JPA query methods.
  • Scans for @Entity classes and configures Spring Data JPA repositories.
  • Can be combined with @AutoConfigureTestDatabase to customize database settings.
  • SQL queries are logged by default, but this can be disabled.
  • Consider using @SpringBootTest with @AutoConfigureTestDatabase for full application configuration with an embedded database.
  • In JUnit 4, use @RunWith(SpringRunner.class) in combination with @DataJpaTest.

@MockMvc

Allows you to send HTTP requests to your controller and check the response, without actually starting a web server or making network requests. This makes it a fast and efficient way to test your controller logic.

@AutoConfigureMockMvc

Automatically configures MockMvc for you, so you don't have to manually set it up.

@AutoConfigureTestDatabase

Configures a test database for testing database-related functionality.

@BeforeEach

  • The @BeforeEach annotation is used to mark a method that should be executed before each test method in the test class.
  • This annotation is typically used to set up the initial state or prepare the environment required for each test method.
  • Common tasks performed in a method annotated with @BeforeEach include initializing objects, setting up mock objects, or preparing test data.
  • Methods annotated with @BeforeEach are executed before each test method, ensuring that the test environment is consistent and isolated for each test.

@AfterEach

  • The @AfterEach annotation is used to mark a method that should be executed after each test method in the test class.
  • This annotation is typically used to clean up resources, reset the state, or perform any necessary teardown tasks after each test method.
  • Common tasks performed in a method annotated with @AfterEach include releasing resources, closing connections, or resetting objects to their initial state.
  • Methods annotated with @AfterEach are executed after each test method, ensuring that any cleanup or teardown tasks are performed consistently after each test.

@Disabled

Disables a test temporarily.

@RepeatedTest

Repeats a test a specified number of times.

@Sql

Executes sql scripts before and after a test

Unit Testing Best Practices

a) Use Test Driven Development Approach ie write the tests before writing the actual code or write them in parallel.

b) Add Tests To your CI CD pipeline and send notifications in case of failure.

c) Ensure tests work in isolation without interfering with each other

d) Ensure a high test coverage of greater than 80% by using test coverage tools like Sonacube and Jacoco .

e) Use meaningful naming convections for testcases. For example itShouldCreateNewUser.

Unit Testing in Action

Step 1: Testing the Repository Class

- Ensure you have a h2 database whose scope is test that will be used as an in memory database in the pom.xml:

<dependency>
			<groupId>com.h2database</groupId>
			<artifactId>h2</artifactId>
			<scope>test</scope>
		</dependency>

- Create a directory inside the test folder and choose resources and configure the h2 database for test inside the application.yml file like below :

spring:
  application:
    name: unit-test-demo
  datasource:
    url: jdbc:h2://mem:db;DB_CLOSE_DELAY=-1
    username: sa
    password: sa
    driver-class-name: org.h2.Driver

Here's what each part of the h2 URL means:

  • jdbc:h2:: This is the protocol prefix for connecting to an H2 database using JDBC.
  • //mem:db: This part indicates that you're connecting to an H2 in-memory database. In-memory databases are created entirely in RAM and are typically used for testing or temporary data storage.
  • DB_CLOSE_DELAY=-1: This is a parameter that specifies how long the database should remain open after the last connection to it is closed. In this case, -1 means that the database will remain open indefinitely, even after all connections are closed. This is useful to keep the in-memory database alive for the duration of your application's lifecycle

Here is the code after writing the unit test inside the EmployeeRepositoryTest file inside the test fder.

@DataJpaTest
class EmployeeRepositoryTest {
    @Autowired
    private EmployeeRepository underTest;

    @AfterEach
    void tearDown() {
        underTest.deleteAll();
    }

    @Test
    void itShouldCheckIfEmployeeExistsByEmail() {
        String email = "johndoe@gmail.com";
        //given
        Employee employee = new Employee(1, "john", "doe", email, "IT");

        underTest.save(employee);
        //when

        boolean expected = underTest.existsByEmail(email);
        //then

        assertThat(expected).isTrue();
    }

    @Test
    void itShouldCheckIfEmployeeDoesNotExistsByEmail() {
        String email = "johndoe@gmail.com";
        //given
        Employee employee = new Employee(1, "john", "doe", email, "IT");

        //when
        boolean expected = underTest.existsByEmail(email);

        //then
        assertThat(expected).isFalse();
    }
}

Step 2:

Unit Testing First Steps

We will create unit tests for each layer of the project. These layers are the Service Layer, Controller Layer and Repository Layer. This is best practice that should be followed even in your projects. We are going to use Mockito and WireMock which are within JUnit to create the test cases. We will use mockito to mock the methods.

share

Victor Ogonyo

Backend and DevOps Engineer