Testing in Spring Boot

In this article, we are convening Spring Boot testing. We are going to discuss the testing provided by Spring Boot.

 

Introduction

Software testing is the process of identifying errors or bugs in a program and checking if the program satisfies the end user’s requirements. Testing is an essential step in the software development process to make sure the quality of the software.

Software testing is of two kinds – unit testing and integration testing. While unit testing focuses on testing small chunks of code (e.g., individual classes) thoroughly, integration testing takes a more end-2-end approach. More details on the different approaches to testing Spring Boot applications is found here.

Both unit and integration test suites can be automated and, typically, are a part of a build process or continuous integration process of a software development project. An effective set of unit and integration tests make sure code extensibility without undesirable side-effects. It is an effective tool in software team productivity when used correctly.

In this article, we are going to discuss Spring boot testing framework. We will briefly look at design practices that allow developers & testers to build good unit and integration test suites and show the methods for building such suites for Spring Boot Applications. Finally, we will round-up some specific annotations provided by Spring Boot to help unit and integration testing. 

 

1. SOLID Principle

The SOLID principles provide guidance for the most effective way to design code that is easily testable. These are:

S – Single Responsibility Principle

A class should do only one thing and it should be the only class in the code that does it.

O – Open/Closed principle

A class should be open to extension, closed to change.

L – Liskov Substitution principle

A derived class shall not modify any behavior common to the base class.

I – Interface Segregation principle

It is a good design to have smaller interfaces, compared to having a few larger interfaces.

D – Dependency inversion principle

A class should depend on abstractions like interfaces and not on concrete implementations.

 

2. Maven Setup

Spring Boot has many annotations and dependencies that come in handy while testing your application. To enable support for testing, add the below dependency to your project’s pom.xml.

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

By adding spring-boot-starter-test dependency, Spring imports the core Spring-boot test modules along with some useful libraries like Hamcrest (library of matcher objects), Mockito (Java mocking framework), JUnit ( unit testing in Java) etc. Read our article on Spring Boot starters for more details.

[pullquote align=”normal”]If the project created as a “Spring starter project” in Eclipse/ Spring Tool Suite (STS) then this dependency is automatically added. [/pullquote]

 

3. Spring Boot Testing

For this article, we will use a simple web-service that provides a REST API. This API accepts a GitHub username and returns a list of the top 5 repositories belonging to the user. It internally uses the Github API and does some post-processing of the results for us.

This example presents a case that is typical in enterprise software projects – an application that has its own logic but also depends on external infrastructure (the Github API in this case). A good test suite will have to test all the application’s logic while requiring a minimal dependency on the production infrastructure. We will build such a test suite, but first, more details about the web service.

The application exposes a REST resource at “/top5/{user}” implemented by SearchController.java.

package com.javadevjournal.repolister.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;

import com.javadevjournal.repolister.model.Repo;
import com.javadevjournal.repolister.service.UserService;

@RestController
@RequestMapping(value = "/top5")
public class SearchController {
 @Autowired
 private UserService userService;

 @RequestMapping(value = "/{user}", method = RequestMethod.GET)
 public List <Repo> getInfo(@PathVariable(name = "user") String user) {
  System.out.println("Querying for [" + user + "]");
  return userService.getResults(user);
 }
}

This class handles the REST API Endpoint and delegates to the UserService that is auto-wired via the Spring Context. The UserService is a simple wrapper on top of the Github API that uses RestTemplate to parse the output and pick the relevant fields.

package com.javadevjournal.repolister.service;

import java.util.Arrays;
import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import com.javadevjournal.repolister.model.Repo;

@Service
public class UserService {
 private static final String search_url = "https://api.github.com/users/%s/repos?page=1&per_page=5";

 @Autowired
 RestTemplate rt;

 public List <Repo> getResults(String user) {
  System.out.println("Invoking: " + String.format(search_url, user));
  Repo[] a = rt.getForEntity(String.format(search_url, user), Repo[].class).getBody();
  return Arrays.asList(a);
 }
}

The Repo class communicate between the UserService and SearchController. It’s a POJO holding the model attributes such as id, name, description, full_name and html_url. In addition, a configuration class injects RestTemplate into the Spring Application Context.

 

4. Testing API

Use Postman or curl to test the AP. Below is a Postman screenshot.

Spring Boot testing

The entire code for this web-service is available on our GitHub repository. It’s set up as a Maven project. You can download it and import into Eclipse or STS and follow along with the steps below. In Eclipse / STS, Choose Run As → Spring Boot App to launch the service.

 

5. Spring Boot Unit Testing

For unit testing, we would like to test the SearchController and UserService classes separately. This way the test will focus only on the functionality in the class being tested and will also not invoke any of its dependencies. Specifically, the unit test would not require access to the external APIs.First, let’s look at the unit test for the SearchController class. We test this class by:

  • Bootstrapping our test with a SpringTestContext by annotating the test class with @RunWith(SpringRunner.class) and @WebMvcTest annotations. The latter is especially used for testing Web-services/ REST Controller classes.
  • Mocking its dependencies using Mockito. In this case, the only dependency is UserService and our test creates a @MockBean that returns static data when SearchController calls userService.getResults() method.
  • Creating an HTTP GET request on our REST API Endpoint using get() method. Use mockMvc.perform() to run this request and return result.
  • Validating the returned data using assertions to decide test success or failure.

Let’s take a look at the source code.

package com.javadevjournal.repolister.controller;

import java.util.ArrayList;
import java.util.List;

import org.junit.BeforeClass;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.skyscreamer.jsonassert.JSONAssert;
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 org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.RequestBuilder;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import com.javadevjournal.repolister.model.Repo;
import com.javadevjournal.repolister.service.UserService;

@RunWith(SpringRunner.class)
@WebMvcTest(value = SearchController.class, secure = false)
public class SearchControllerTest {

 @Autowired
 private MockMvc mockMvc;

 @MockBean
 private UserService userService;

 private static List <Repo> repoList;

 @BeforeClass
 public static void setupTestData() {
  repoList = new ArrayList <Repo> ();
  // Populate with test data
  repoList.add(new Repo("1", "Repo1", "Repository 1", "http://myurl.com/1", "Description 1"));
  repoList.add(new Repo("2", "Repo2", "Repository 2", "http://myurl.com/2", "Description 2"));
  repoList.add(new Repo("3", "Repo3", "Repository 3", "http://myurl.com/3", "Description 3"));
  repoList.add(new Repo("4", "Repo4", "Repository 4", "http://myurl.com/4", "Description 4"));
  repoList.add(new Repo("5", "Repo5", "Repository 5", "http://myurl.com/5", "Description 5"));
 }

 @Test
 public void testGetInfo() throws Exception {
  String apiUrl = "/top5/tester";
  // Setup "Mockito" to mock userService call
  Mockito.when(userService.getResults(Mockito.anyString())).thenReturn(repoList);

  // Build a GET Request and send it to the test server
  RequestBuilder rb = MockMvcRequestBuilders.get(apiUrl).accept(MediaType.APPLICATION_JSON);
  MvcResult r = mockMvc.perform(rb).andReturn(); // throws Exception

  // Validate response
  String tr = r.getResponse().getContentAsString();
  // System.out.println(tr);

  String er = "[{\"id\":\"1\",\"name\":\"Repo1\",\"full_name\":\"Repository 1\",\"html_url\":\"http://myurl.com/1\",\"description\":\"Description 1\"},{\"id\":\"2\",\"name\":\"Repo2\",\"full_name\":\"Repository 2\",\"html_url\":\"http://myurl.com/2\",\"description\":\"Description 2\"},{\"id\":\"3\",\"name\":\"Repo3\",\"full_name\":\"Repository 3\",\"html_url\":\"http://myurl.com/3\",\"description\":\"Description 3\"},{\"id\":\"4\",\"name\":\"Repo4\",\"full_name\":\"Repository 4\",\"html_url\":\"http://myurl.com/4\",\"description\":\"Description 4\"},{\"id\":\"5\",\"name\":\"Repo5\",\"full_name\":\"Repository 5\",\"html_url\":\"http://myurl.com/5\",\"description\":\"Description 5\"}]";
  JSONAssert.assertEquals(er, tr, true);

  // Or we can use JUnit's assertEquals() method
  // assertEquals("REST API Returned incorrect response.", er, tr);
 }
}

Similarly, to unit test UserService.java we can create a JUnit test class that mocks the RestTemplate object by returning static data and make sure the UserService is able to process it. Notice how the unit tests specifically focus on the individual functionality of the classes.

For example, the SearchControllerTest checks the end-point implementation behavior only without knowledge of the behavior of other parts of the application. This is possible because the code adheres to the Single Responsibility principle outlined earlier.

 

6. Spring Boot Integration Testing

To enable testing a Spring boot application or parts of it, Spring boot has @SpringBootTest annotation and provides the following features:

  • It uses SpringBootContextLoader even if @ContextConfiguration is not specified.
  • If no explicit class is specified or @Configuration is not used, @SpringBootTest annotation searches for @SpringBootConfiguration automatically.
  • Support to start a web environment where a server listens to a random port is provided.
  • Register a WebTestClient bean for performing web tests in a web server that is fully running.

For this application, we have created an integration test that tests the end-to-end functionality by invoking the REST API and validating its results.

package com.javadevjournal.repolister;

import static org.hamcrest.Matchers.*;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.*;

import org.junit.Before;
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.http.MediaType;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import com.javadevjournal.repolister.controller.SearchController;


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

 @Autowired
 SearchController sc

 private MockMvc mockMvc;
 private String apiUrl = "/top5/%s";

 @Before
 public void setup() throws Exception {
  // Setup application context and build mockMvc
  this.mockMvc = MockMvcBuilders.standaloneSetup(this.sc).build();
 }

 @Test
 public void testApiResponse() throws Exception {

  // Send an API request and validate response (Headers + Content)
  mockMvc.perform(get(String.format(apiUrl, "octocat")))
   .andExpect(status().isOk())
   .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
   .andExpect(jsonPath("$", hasSize(5)))
   .andExpect(jsonPath("$.*.name", hasItem(is("linguist"))))
   .andExpect(jsonPath("$.*.html_url", hasItem(is("https://github.com/octocat/linguist"))));

 }

 @Test
 public void testUserWithLargeRepoList() throws Exception {
  // Test if API picks the right repo from a user with large number of repos
  mockMvc.perform(get(String.format(apiUrl, "umeshawasthi")))
   .andExpect(status().isOk())
   .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
   .andExpect(jsonPath("$", hasSize(5)))
   .andExpect(jsonPath("$.*.name", hasItem(is("blooddonor"))))
   .andExpect(jsonPath("$.*.html_url", hasItem(is("https://github.com/umeshawasthi/blooddonor"))));
 }

 @Test
 public void testUserWithSmallRepoList() throws Exception {
  // Test if API handles user with <5 repos correctly (note change in hasSize(...))
  mockMvc.perform(get(String.format(apiUrl, "madhuri2k")))
   .andExpect(status().isOk())
   .andExpect(content().contentTypeCompatibleWith(MediaType.APPLICATION_JSON))
   .andExpect(jsonPath("$", hasSize(lessThanOrEqualTo(5))))
   .andExpect(jsonPath("$.*.name", hasItem(is("madhuri2k.github.io"))))
   .andExpect(jsonPath("$.*.html_url", hasItem(is("https://github.com/madhuri2k/fantastic-spoon"))));
 }

}

The test uses concepts discussed already but tests the SearchController and UserService classes in the context of their use in the web application. This test also requires accessing the external Github API to run.

The following are other annotations useful while creating unit tests in JUnit.

  • @ExpectedException – This annotation allows the test method to throw a specific exception. If no exception is thrown, the test will fail.
  • @Timed – A timeout is set for the method that is tested and the test fails if the test has not completed running before the timeout.
  • @Repeat – Allows multiple repetitions of a test method.

 

Summary

In this post, we got a basic understanding of the Spring Boot testing framework. We saw how to create a test suite for a simple web-service. We looked at the different testing approaches and how good software design enables leveraging unit and integration tests to catch bugs early. Finally, we implemented the unit and integration tests on a Spring Boot Application and checked application functionality via these tests. 

Which of your projects could benefit from adding unit/integration test cases? How do you see these practices getting applied in your own projects? Feel free to comment or ask any questions you may have. You can download the code for this project from GitHub