Unit Testing with Spring Boot Application

In this article, we will show you how to test various layers of Spring Boot 2 application. We will use Order management application created in previous post ‘Create a REST API with Spring Boot‘ for this article. Main technologies used to unit test Spring Boot application are JUnit 5,  Mockito,  DataJpaTest and WebMvcTest.

Maven Test dependencies

<!--Spring Boot test-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
    <exclusions>
        <exclusion>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
        </exclusion>
    </exclusions>
</dependency>

<!--Junit 5-->
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-engine</artifactId>
    <scope>test</scope>
</dependency>

<!-- In Memory database useful for testing-->
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
    <scope>runtime</scope>
</dependency>

Layers to be Unit tested

  • Controller Layer
  • Service Layer
  • Repository Layer

Testing Controller Layer

The aim of controller Test is to test controller in isolation while mocking dependent services.

UserControllerTest

@WebMvcTest(value = UserController.class)
class UserControllerTest {

    @MockBean
    private UserService userService;

    @Autowired
    private MockMvc mockMvc;
    private User user;
    private User user2;

    @Autowired
    private ObjectMapper objectMapper;

    @BeforeEach
    void setup() {
        user = new User();
        user.setFirstName("Steve");
        user.setLastName("Rob");
        user.setEmail("steverob@gmail.com");
        user.setId(1L);
        user.setFirstLineOfAddress("12 Avenue");
        user.setSecondLineOfAddress("Commercial street");
        user.setTown("London");
        user.setPostCode("HA6 0EW");

        user2 = new User();
        user2.setFirstName("Stev");
        user2.setLastName("Ro");
        user2.setEmail("stevob@gmail.com");
        user2.setId(2L);
        user2.setFirstLineOfAddress("13 Avenue");
        user2.setSecondLineOfAddress("Commercial street");
        user2.setTown("London");
        user2.setPostCode("HA7 0EW");

    }

    @Test
    void testGetUser() throws Exception {
        String expected = objectMapper.writeValueAsString(user);
        when(userService.getUserById(anyLong())).thenReturn(user);
        RequestBuilder requestBuilder = MockMvcRequestBuilders.get(
                "/api/users/" + user.getId()).accept(
                MediaType.APPLICATION_JSON);
        MvcResult mvcResult = mockMvc.perform(requestBuilder)
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andReturn();

        assertEquals(expected, mvcResult.getResponse().getContentAsString());
    }

    @Test
    public void testGetAllUsers() throws Exception {

        List<User> users = Arrays.asList(user, user2);
        String expected = objectMapper.writeValueAsString(users);

        when(userService.getUsers()).thenReturn(users);
        RequestBuilder requestBuilder = MockMvcRequestBuilders.get(
                "/api/users").accept(MediaType.APPLICATION_JSON);
        MvcResult mvcResult = mockMvc.perform(requestBuilder)
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andReturn();

        assertEquals(expected, mvcResult.getResponse().getContentAsString());
    }

    @Test
    void testCreateUser() throws Exception {
        String expected = objectMapper.writeValueAsString(user);
        when(userService.createUser(any(User.class))).thenReturn(user);
        RequestBuilder requestBuilder = MockMvcRequestBuilders.post("/api/users")
                .contentType(MediaType.APPLICATION_JSON)
                .content(objectMapper.writeValueAsString(user))
                .accept(MediaType.APPLICATION_JSON);

        MvcResult mvcResult = mockMvc.perform(requestBuilder)
                .andExpect(MockMvcResultMatchers.status().isCreated())
                .andReturn();
        assertEquals(expected, mvcResult.getResponse().getContentAsString());

    }

    @Test
    void testUpdateUser() throws Exception {
        User updateUser = new User();
        updateUser.setFirstName("Steveupdated");
        updateUser.setLastName("Robupdated");
        updateUser.setEmail("steverobupdated@gmail.com");
        updateUser.setFirstLineOfAddress("12 Avenueupdated");
        updateUser.setSecondLineOfAddress("Commercial streetupdated");
        updateUser.setTown("London");
        updateUser.setPostCode("HA6 0EW");

        User updateUserWithId = new User();
        new ModelMapper().map(updateUser, updateUserWithId);
        updateUserWithId.setId(1L);
        //Exclude null values
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        String putBody = objectMapper.writeValueAsString(updateUser);
        String expectedResponse = objectMapper.writeValueAsString(updateUserWithId);

        when(userService.updateUser(anyLong(), any(User.class))).thenReturn(updateUserWithId);
        RequestBuilder requestBuilder = MockMvcRequestBuilders.put("/api/users/{userId}",
                updateUserWithId.getId())
                .contentType(MediaType.APPLICATION_JSON)
                .content(putBody)
                .accept(MediaType.APPLICATION_JSON);

        MvcResult mvcResult = mockMvc.perform(requestBuilder)
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andReturn();
        assertEquals(expectedResponse, mvcResult.getResponse().getContentAsString());

    }

    @Test
    void testPatchUpdateUser() throws Exception {
        User updateUser = new User();
        updateUser.setFirstName("Steveupdated");
        updateUser.setLastName("Robupdated");

        user.setFirstName(updateUser.getFirstName());
        user.setLastName(updateUser.getLastName());

        //Exclude null values
        objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
        String patchBody = objectMapper.writeValueAsString(updateUser);
        String expectedResponse = objectMapper.writeValueAsString(user);

        when(userService.patchUpdateUser(anyLong(), any(User.class))).thenReturn(user);
        RequestBuilder requestBuilder = MockMvcRequestBuilders.patch("/api/users/{userId}", user.getId())
                .contentType(MediaType.APPLICATION_JSON)
                .content(patchBody)
                .accept(MediaType.APPLICATION_JSON);

        MvcResult mvcResult = mockMvc.perform(requestBuilder)
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andReturn();
        assertEquals(expectedResponse, mvcResult.getResponse().getContentAsString());

    }

    @Test
    void testDeleteUser() throws Exception {
        doNothing().when(userService).deleteUser(anyLong());
        RequestBuilder requestBuilder = MockMvcRequestBuilders.delete("/api/users/" + user.getId())
                .accept(MediaType.APPLICATION_JSON);
        MvcResult mvcResult = mockMvc.perform(requestBuilder)
                .andExpect(MockMvcResultMatchers.status().isNoContent())
                .andReturn();
        assertEquals("", mvcResult.getResponse().getContentAsString());

    }

}

@WebMvcTest(value = UserController.class): This tells Spring Boot to fire up application context with only beans required for this controller.

@MockBean: Automatically replaces bean with mockito mock. For unit testing we don’t need real services hence using mock objects.

@Autowired: Automatically create real instance into the concerned class.

@BeforeEach: Setup method triggered before each test

@Test: Indicates JUnit test method

objectMapper.writeValueAsString(user): Used to convert object to Json string

when(userService.getUserById(anyLong())).thenReturn(user): Configures mocked services to returns predefined object when mocked object’s method is called.

RequestBuilder requestBuilder = MockMvcRequestBuilders.post(“/api/users”) : Creates request builder with body content and headers and uses the same to perform HTTP request. Also verifies expected status code.

assertEquals(expected, mvcResult.getResponse().getContentAsString()): verifies response body

objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); String patchBody = objectMapper.writeValueAsString(updateUser); :

Above two statements says that only non null values of updateUser object should be used for serialisation as we don’t want null property values in request body.

Rest of the code is self explanatory.

Testing Service Layer

The aim of Service Test is to test service in isolation while mocking dependent repository

UserServiceImplTest

@ExtendWith(SpringExtension.class)
class UserServiceImplTest {

    @InjectMocks
    private UserServiceImpl userService;

    @Mock
    private UserRepository userRepository;

    private User user;
    private User user2;

    @BeforeEach
    void setUp() {
        MockitoAnnotations.initMocks(this);

        user = new User();
        user.setId(1L);
        user.setFirstName("Steve");
        user.setLastName("Rob");
        user.setEmail("steverob@gmail.com");
        user.setFirstLineOfAddress("10 Oliver");
        user.setSecondLineOfAddress("Commercial street");
        user.setTown("London");
        user.setPostCode("HA4 7EW");

        user2 = new User();
        user2.setId(2L);
        user2.setFirstName("Tony");
        user2.setLastName("Taylor");
        user2.setEmail("tonytaylor@gmail.com");
        user2.setFirstLineOfAddress("12 Avenue");
        user2.setSecondLineOfAddress("Avenue street");
        user2.setTown("London");
        user2.setPostCode("HA6 0EW");

    }

    @Test
    void testGetUserById() {
        when(userRepository.findById(anyLong())).thenReturn(Optional.of(user));

        User userFound = userService.getUserById(user.getId());
        assertNotNull(userFound);
        assertEquals(user.getId(), userFound.getId());
        assertEquals(user.getEmail(), userFound.getEmail());
        assertEquals(user.getFirstName(), userFound.getFirstName());
        assertEquals(user.getLastName(), userFound.getLastName());
        assertEquals(user.getPostCode(), userFound.getPostCode());
        assertEquals(user.getFirstLineOfAddress(), userFound.getFirstLineOfAddress());
        assertEquals(user.getSecondLineOfAddress(), userFound.getSecondLineOfAddress());
        assertEquals(user.getTown(), userFound.getTown());
    }

    @Test
    void shouldThrowExceptionWhenGetUserHasWrongId() {
        when(userRepository.findById(anyLong())).thenReturn(Optional.empty());
        assertThrows(RuntimeException.class, () -> userService.getUserById(10000L));
    }

    @Test
    void testGetUsers() {

        List<User> users = Arrays.asList(user, user2);

        when(userRepository.findAll()).thenReturn(users);

        List<User> usersReceived = userService.getUsers();

        assertEquals(users, usersReceived);
        assertEquals(users.size(), usersReceived.size());
        assertEquals(users.get(0).getId(), usersReceived.get(0).getId());
        assertEquals(users.get(0).getFirstName(), usersReceived.get(0).getFirstName());
        assertEquals(users.get(0).getLastName(), usersReceived.get(0).getLastName());
        assertEquals(users.get(0).getEmail(), usersReceived.get(0).getEmail());
        assertEquals(users.get(0).getFirstLineOfAddress(), usersReceived.get(0).getFirstLineOfAddress());
        assertEquals(users.get(0).getSecondLineOfAddress(), usersReceived.get(0).getSecondLineOfAddress());
        assertEquals(users.get(0).getSecondLineOfAddress(), usersReceived.get(0).getSecondLineOfAddress());
    }

    @Test
    void testCreateUser() {
        when(userRepository.findByEmail(anyString())).thenReturn(Optional.empty());
        when(userRepository.save(any(User.class))).thenReturn(user);

        User createNewUser = new User();
        createNewUser.setFirstName(user.getFirstName());
        createNewUser.setLastName(user.getLastName());
        createNewUser.setEmail(user.getEmail());
        createNewUser.setPostCode(user.getPostCode());
        createNewUser.setFirstLineOfAddress(user.getFirstLineOfAddress());
        createNewUser.setSecondLineOfAddress(user.getSecondLineOfAddress());
        createNewUser.setTown(user.getTown());

        User createdUser = userService.createUser(createNewUser);

        assertNotNull(createdUser);
        assertEquals(user.getId(), createdUser.getId());
        assertEquals(user.getEmail(), createdUser.getEmail());
        assertEquals(user.getFirstName(), createdUser.getFirstName());
        assertEquals(user.getLastName(), createdUser.getLastName());
        assertEquals(user.getPostCode(), createdUser.getPostCode());
        assertEquals(user.getFirstLineOfAddress(), createdUser.getFirstLineOfAddress());
        assertEquals(user.getSecondLineOfAddress(), createdUser.getSecondLineOfAddress());
        assertEquals(user.getTown(), createdUser.getTown());
    }

    @Test
    void testUpdateUser() {

        User updateUser = new User();
        updateUser.setFirstName("Steve");
        updateUser.setLastName("Rob");
        updateUser.setEmail("steverob@gmail.com");
        updateUser.setPostCode("HA5 2EW");
        updateUser.setFirstLineOfAddress("10 Oliver");
        updateUser.setSecondLineOfAddress("Commercial street");
        updateUser.setTown("London");

        User updateUserWithId = new User();
        new ModelMapper().map(updateUser, updateUserWithId);
        updateUserWithId.setId(1L);

        when(userRepository.findById(anyLong())).thenReturn(Optional.of(updateUserWithId));

        when(userRepository.save(any(User.class))).thenReturn(updateUserWithId);

        User savedUser = userService.updateUser(updateUserWithId.getId(), updateUser);

        assertNotNull(savedUser);
        assertEquals(updateUserWithId.getId(), savedUser.getId());
        assertEquals(updateUserWithId.getEmail(), savedUser.getEmail());
        assertEquals(updateUserWithId.getFirstName(), savedUser.getFirstName());
        assertEquals(updateUserWithId.getLastName(), savedUser.getLastName());
        assertEquals(updateUserWithId.getPostCode(), savedUser.getPostCode());
        assertEquals(updateUserWithId.getFirstLineOfAddress(), savedUser.getFirstLineOfAddress());
        assertEquals(updateUserWithId.getSecondLineOfAddress(), savedUser.getSecondLineOfAddress());
        assertEquals(updateUserWithId.getTown(), savedUser.getTown());
    }

    @Test
    void testPatchUpdateUser() {

        User updateUser = new User();
        updateUser.setFirstName("Steveupdated");
        updateUser.setLastName("Robupdated");

        user.setFirstName(updateUser.getFirstName());
        user.setLastName(updateUser.getLastName());

        when(userRepository.findById(anyLong())).thenReturn(Optional.of(user));

        when(userRepository.save(any(User.class))).thenReturn(user);

        User savedUser = userService.patchUpdateUser(user.getId(), updateUser);

        assertNotNull(savedUser);
        assertEquals(user.getId(), savedUser.getId());
        assertEquals(user.getEmail(), savedUser.getEmail());
        assertEquals(user.getFirstName(), savedUser.getFirstName());
        assertEquals(user.getLastName(), savedUser.getLastName());
        assertEquals(user.getPostCode(), savedUser.getPostCode());
        assertEquals(user.getFirstLineOfAddress(), savedUser.getFirstLineOfAddress());
        assertEquals(user.getSecondLineOfAddress(), savedUser.getSecondLineOfAddress());
        assertEquals(user.getTown(), savedUser.getTown());
    }

    @Test
    void testDeleteUser() {

        when(userRepository.findById(anyLong())).thenReturn(Optional.of(user));
        doNothing().when(userRepository).delete(any(User.class));

        userService.deleteUser(user.getId());

        verify(userRepository, times(1)).findById(user.getId());
        verify(userRepository, times(1)).delete(user);

    }

}

@ExtendWith(SpringExtension.class):Integrates Spring TestContext with JUnit 5 Test. This annotation is not required when @DataJpaTest, @WebMvcTest, and @SpringBootTest  are used, as it already included.

@InjectMocks: Indicates mocked object will be injected.

@Mock: Indicates mock object will be used instead of real objects. MockitoAnnotations.initMocks(this): Initiates all mocks in the class.

Rest of the code is self explanatory.

Testing Repository Layer

The aim of Repository Test is to test repository in isolation. In memory database is used instead of real database.

UserRepositoryTest

@DataJpaTest
class UserRepositoryTest {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private TestEntityManager entityManager;

    @Test
    void testFndByEmail() {
        //Given
        User user = new User();
        user.setFirstName("Steve");
        user.setLastName("Rob");
        user.setEmail("steverob@gmail.com");
        user.setFirstLineOfAddress("10 Oliver");
        user.setSecondLineOfAddress("Commercial street");
        user.setTown("London");
        user.setPostCode("HA4 7EW");
        entityManager.persist(user);
        entityManager.flush();
        //When
        User found = userRepository.findByEmail(user.getEmail()).get();
        //Then
        assertEquals(user.getEmail(), found.getEmail());
    }

}

@DataJpaTest: Sets up H2, in memory database against which all queries will be run. It sets up Hibernate, Spring Data and data source.

entityManager.persist(user); entityManager.flush(): Creates data in the H2 database

Run all tests

mvn test

Complete Source code including tests location

https://github.com/ranjesh1/spring-boot2-rest-order-services.git