Testing with Spring Boot

Testing with Spring Boot

·

7 min read

In my previous article, we created a Spring Boot REST API application, but I didn't include the tests. In the real world, we would have to write them, so in this article, I will show how to write Unit Tests on a Spring Boot application.

What Is Unit Testing?

Unit Testing is how software engineers can test a single piece of code in a codebase. It's a type of test that allows developers to identify problems at an earlier stage.

What Is Integration Testing?

Integration Testing is a type of testing used to ensure that all the integration points are working fine and each system is interacting with each other correctly.

Let's start by updating the pom.xml by adding the relevant dependencies.

        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.1.0</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.6.2</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>com.h2database</groupId>
            <artifactId>h2</artifactId>
            <scope>test</scope>
        </dependency>

We added:

  • Junit5: JUnit is a Java library that allows you to write and run automated tests.

  • AssertJ: AssertJ is a Java library that allows us to write assertion statements.

  • H2 database: this is a Java in-memory relational database management system mainly used for testing purposes.

Mockito is going to be added manually.

This is the package structure under the test folder.

test-package-structure.png

Creating the Abstract Test Class

package com.techwithmaddy.CustomerAPI.controllertest;

import com.fasterxml.jackson.core.JsonParseException;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.test.context.web.WebAppConfiguration;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;

import java.io.IOException;

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
public abstract class AbstractTest {

    protected MockMvc mvc;

    @Autowired
    protected ObjectMapper objectMapper;

    @Autowired
    WebApplicationContext webApplicationContext;

    protected void setUp() {
       mvc = MockMvcBuilders.standaloneSetup(new CustomerController()).build();
    }

    protected String mapToJson(Object obj) throws JsonProcessingException {
        ObjectMapper objectMapper = new ObjectMapper();
        return objectMapper.writeValueAsString(obj);
    }

    protected <T> T mapFromJson(String json, Class<T> clazz)
            throws JsonParseException, JsonMappingException, IOException {

        ObjectMapper objectMapper = new ObjectMapper();
        return objectMapper.readValue(json, clazz);
    }
}

To test the REST controller, we need to create an abstract class that we can use to create the web application context (to keep this straight, it's a configuration class for web applications. You can read more here).

In this class, we have:

  1. MockMvc is a class part of the Spring MVC framework. We'll need this to test the REST controller.

  2. The setup method where do a standalone setup because we only want to consider the CustomerController for our application.

  3. MapToJson is a method to convert a Java object into JSON.

  4. MapFromJson is a method to convert from JSON to Java objects.

Testing the Rest Controller

The Unit Test for the REST Controller would look like this:

package com.techwithmaddy.CustomerAPI.Controller;

import com.techwithmaddy.CustomerAPI.controller.CustomerController;
import com.techwithmaddy.CustomerAPI.controllertest.AbstractTest;
import com.techwithmaddy.CustomerAPI.exception.CustomerNotFoundException;
import com.techwithmaddy.CustomerAPI.model.Customer;
import com.techwithmaddy.CustomerAPI.service.CustomerService;
import org.junit.Test;
import org.junit.runner.RunWith;
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.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;

import java.util.Optional;

import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@RunWith(SpringRunner.class)
@WebMvcTest(CustomerController.class)
public class CustomerControllerTest extends AbstractTest {

    @Autowired
    private MockMvc mockMvc;

    @MockBean
    private CustomerService customerService;

    @Test
    public void shouldSaveCustomer() throws Exception {
        Customer customer = new Customer();
        customer.setFirstName("firstName");
        customer.setLastName("lastName");
        customer.setEmail("email@test.com");
        customer.setPhoneNumber("0123456789");

        mockMvc.perform(post("/customer/save")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(customer)))
                .andExpect(status().isOk());

    }

    @Test
    public void shouldGetCustomerByEmail() throws Exception {
        Customer customer = new Customer();
        String email = "steve@austin.com";
        Optional<Customer> customerOptional = Optional.of(customer);

        when(customerService.getCustomerByEmail(email)).thenReturn(customerOptional);

        mockMvc.perform(get(String.format("/customer/retrieve"))
                .contentType(MediaType.APPLICATION_JSON)
                .queryParam("email", email))
                .andExpect(status().isOk());

    }

    @Test
    public void shouldThrowExceptionIfEmailNotFound() throws Exception {
        String email = "test@email.com";

        doThrow(new CustomerNotFoundException()).when(customerService).getCustomerByEmail(email);

        mockMvc.perform(get(String.format("/customer/retrieve"))
                .contentType(MediaType.APPLICATION_JSON)
                .queryParam("email", email))
                .andExpect(status().isNotFound());

    }


}

This class extends the AbstractTest class that we created before.

  • @ RunWith(SpringRunner.class) is an annotation that gives us Spring testing functionality.

  • @ WebMvcTest is an annotation used for Spring MVC tests only.

We inject MockMvc and mock CustomerService as it's a dependency.

  • Each test is annotated with the @Test annotation to say that we want a specific piece of code to run as a test case.

The Controller class is where we handle all the incoming HTTP requests.

To test the REST Controller, we create a mock of the incoming request.

Let's look at the shouldSaveCustomer() test.

    @Test
    public void shouldSaveCustomer() throws Exception {
        Customer customer = new Customer();
        customer.setFirstName("firstName");
        customer.setLastName("lastName");
        customer.setEmail("email@test.com");
        customer.setPhoneNumber("0123456789");

        mockMvc.perform(post("/customer/save")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(customer)))
                .andExpect(status().isOk());

    }

The mockMvc.perform works like this as per the documentation :

Perform a request and return a type that allows chaining further actions, such as asserting expectations, on the result.

  • To save a customer, we use the POST request.

  • A POST request creates new data, and in this case, we are mocking the request of saving a new customer into the database.

  • To save a customer into the database, we use the /customer/save endpoint.

  • The body of the content type is of type JSON.

  • We're converting the Java customer object to JSON.

  • We expect a 200 status code (= OK).

shouldGetByEmail() test follows a similar logic:

    @Test
    public void shouldGetCustomerByEmail() throws Exception {
        Customer customer = new Customer();
        String email = "steve@austin.com";
        Optional<Customer> customerOptional = Optional.of(customer);

        when(customerService.getCustomerByEmail(email)).thenReturn(customerOptional);

        mockMvc.perform(get(String.format("/customer/retrieve"))
                .contentType(MediaType.APPLICATION_JSON)
                .queryParam("email", email))
                .andExpect(status().isOk());

    }
  • We want to retrieve a customer using their email.

  • We use the when clause to retrieve the customer by email that will return a customer with that email.

  • The mock request is a GET request which uses the /customer/retrieve endpoint.

  • The body of the content type is of type JSON.

  • We're adding the email as the query parameter.

  • We expect a 200 status code (= OK).

Similar logic for the shouldThrowExceptionIfEmailNotFound() test.

    @Test
    public void shouldThrowExceptionIfEmailNotFound() throws Exception {
        String email = "test@email.com";

        doThrow(new CustomerNotFoundException()).when(customerService).getCustomerByEmail(email);

        mockMvc.perform(get(String.format("/customer/retrieve"))
                .contentType(MediaType.APPLICATION_JSON)
                .queryParam("email", email))
                .andExpect(status().isNotFound());

    }
  • We want to test that if the email doesn't exist, we throw a CustomerNotFoundException.

  • We use the doThrow clause that will throw the Exception when we call the getCustomerByEmail from the service class with an email that doesn't exist.

  • The mock request is a GET request.

  • The body of the content type is of type JSON.

  • We're adding the email as the query parameter.

  • We expect a 404 status code (Not Found).

Testing the Service Layer

For the testing layer, we can write integration tests to ensure that all components work as expected. The service layer communicates with the repository layer.

package com.techwithmaddy.CustomerAPI.service;

import com.techwithmaddy.CustomerAPI.model.Customer;
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.test.context.junit4.SpringRunner;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertEquals;

@RunWith(SpringRunner.class)
@SpringBootTest
public class CustomerServiceTest {

    @Autowired
    private CustomerService customerService;

    @Test
    public void shouldSaveCustomerSuccessfully() {
        Customer customer = new Customer();
        customer.setFirstName("Richard");
        customer.setLastName("Branson");
        customer.setEmail("richard@branson.com");
        customer.setPhoneNumber("0112233445566");

        Customer savedCustomer = customerService.saveCustomer(customer);

        assertThat(savedCustomer).isNotNull();

    }

    @Test
    public void shouldGetCustomerByEmail() {
        Customer customer = new Customer();
        customer.setFirstName("Steve");
        customer.setLastName("Austin");
        customer.setEmail("steve@austin.com");
        customer.setPhoneNumber("01223344556");

        Optional<Customer> retrievedCustomer = customerService.getCustomerByEmail(customer.getEmail());

        assertEquals(retrievedCustomer.get().getFirstName(), customer.getFirstName());
        assertEquals(retrievedCustomer.get().getLastName(), customer.getLastName());
        assertEquals(retrievedCustomer.get().getEmail(), customer.getEmail());
        assertEquals(retrievedCustomer.get().getPhoneNumber(), customer.getPhoneNumber());

    }
}

Let's look at the test shouldSaveCustomerSuccessfully()

    @Test
    public void shouldSaveCustomerSuccessfully() {
        Customer customer = new Customer();
        customer.setFirstName("Richard");
        customer.setLastName("Branson");
        customer.setEmail("richard@branson.com");
        customer.setPhoneNumber("0112233445566");

        Customer savedCustomer = customerService.saveCustomer(customer);

        assertThat(savedCustomer).isNotNull();

    }

In short, we want to test that this customer gets successfully saved into the database. If you run this test, Richard Branson should appear in the database table.

richard_saved.png

shouldGetCustomerByEmail test follows a similar logic:

     @Test
    public void shouldGetCustomerByEmail() {
        Customer customer = new Customer();
        customer.setFirstName("Steve");
        customer.setLastName("Austin");
        customer.setEmail("steve@austin.com");
        customer.setPhoneNumber("01223344556");

        Optional<Customer> retrievedCustomer = customerService.getCustomerByEmail(customer.getEmail());

        assertEquals(retrievedCustomer.get().getFirstName(), customer.getFirstName());
        assertEquals(retrievedCustomer.get().getLastName(), customer.getLastName());
        assertEquals(retrievedCustomer.get().getEmail(), customer.getEmail());
        assertEquals(retrievedCustomer.get().getPhoneNumber(), customer.getPhoneNumber());

    }
  • We want to test that the data is the same as the data in the database. If any of these fields is incorrect, the test will not pass.

steve_test_passed.png

Testing the Repository Layer

package com.techwithmaddy.CustomerAPI.repository;

import com.techwithmaddy.CustomerAPI.model.Customer;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.junit4.SpringRunner;

import static org.assertj.core.api.Assertions.assertThat;

@RunWith(SpringRunner.class)
@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
public class CustomerRepositoryTest {

    @Autowired
    private CustomerRepository customerRepository;

    @Test
    public void shouldFindCustomerByEmail() {
        Customer customer = customerRepository.findCustomerByEmail("richard@branson.com");
        assertThat(customerRepository.findCustomerByEmail(customer.getEmail())).isEqualTo(customer);
    }
}
  • This class is also annotated with the @ DataJpaTest, an annotation used specifically to test JPA components.

  • Then, we check that the email has an existing customer, which it does. If the email doesn't exist, the test would fail.

ADDITIONAL REFERENCES:

You can find the complete Github repository here.

I hope you've found this helpful. Let me know your feedback in the comments.

Until next time! 👋🏾

Did you find this article valuable?

Support Maddy by becoming a sponsor. Any amount is appreciated!