Spring Boot Testing Best Practices

Proper testing is critical to the successful development of applications that use a microservices architecture. This guide provides some important recommendations for writing tests for Spring Boot applications, using F.I.R.S.T. principles:

  • F - Fast
  • I - Independent
  • R - Repeatable
  • S - Self-Validating
  • T - Timely

Isolate the functionality to be tested

You can better isolate the functionality you want to test by limiting the context of loaded frameworks/components. Often, it is sufficient to use the JUnit unit testing framework. without loading any additional frameworks. To accomplish this, you only need to annotate your test with @Test.

In the very naive code snippet below, there are no database interactions, and MapRepository loads data from the classpath.

import org.junit.Test;

import static org.junit.Assert.assertEquals;

public class MapRepositoryTest {

	private MapRepository mapRepository = new MapRepository();

	@Test
	public void shouldReturnJurisdictionForZip() {
    	final String expectedJurisdiction = "NJ";
    	assertEquals(expectedJurisdiction, mapRepository.findByZip("07677"));
	}
}

As a next step up in complexity, consider adding mock frameworks, like those generated by the Mockito mocking framework, if you have interactions with external resources. Using mock frameworks eliminates the need to access real instances of external resources.

import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;

import java.util.Date;

import static org.mockito.Matchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;

@RunWith(MockitoJUnitRunner.class)
public class CarServiceTest {

	private CarService carService;

	@Mock
	private RateFinder rateFinder;

	@Before
	public void init() {
    	carService = new CarService(rateFinder);
	}

	@Test
	public void shouldInteractWithRateFinderToFindBestRate() {
    	carService.schedulePickup(new Date(), new Route());
    	verify(rateFinder, times(1)).findBestRate(any(Route.class));
	}
}

Only load slices of functionality

@SpringBootTest Annotation

When testing spring boot applications, the @SpringBootTest annotation loads the whole application, but it is often better to limit the application context to just the set of Spring components that participate in the test scenario. This is accomplished by listing them in the annotation declaration.

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.Date;

import static org.junit.Assert.assertTrue;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = {MapRepository.class, CarService.class})
public class CarServiceWithRepoTest {

	@Autowired
	private CarService carService;

	@Test
	public void shouldReturnValidDateInTheFuture() {
    	Date date = carService.schedulePickup(new Date(), new Route());
    	assertTrue(date.getTime() > new Date().getTime());
	}
}

@DataJpaTest Annotation

Using @DataJpaTest only loads @Repository spring components, and will greatly improve performance by not loading @Service, @Controller, etc.

@RunWith(SpringRunner.class)
@DataJpaTest
public class MapTests {

	@Autowired
	private MapRepository repository;

	@Test
	public void findByUsernameShouldReturnUser() {
    	final String expected = "NJ";
    	String actual = repository.findByZip("07677")

    	assertThat(expected).isEqualTo(actual);
	}
}

Sometimes, the Table Already Exists exception is thrown when testing with an H2 database. This is an indication that H2 was not cleared between test invocations. This behavior has been observed when combining database tests with initialization of the API mocking tool WireMock. It could also occur if multiple qualifying schema-.sql files are located in the classpath.

It is a good practice to mock the beans that are involved in database interactions, and turn off Spring Boot test db initialization for the Spring profile that tests run. You should strongly consider this when testing Controllers. Alternatively, you can try to declare your table creation DDL in schema.sql files as CREATE TABLE IF NOT EXISTS.

spring.datasource.initialize=false

spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.orm.jpa.HibernateJpaAutoConfiguration,\
	org.springframework.boot.autoconfigure.jdbc.DataSourceAutoConfiguration,\
	org.springframework.boot.autoconfigure.jdbc.DataSourceTransactionManagerAutoConfiguration,\
	org.springframework.boot.autoconfigure.data.web.SpringDataWebAutoConfiguration

Test the web layer

Use @WebMvcTest to test REST APIs exposed through Controllers without the server part running. Only list Controllers that are being tested.

Note: It looks like Spring Beans used by a Controller need to be mocked.

@RunWith(SpringRunner.class)
@WebMvcTest(CarServiceController.class)
public class CarServiceControllerTests {

	@Autowired
	private MockMvc mvc;

	@MockBean
	private CarService carService;

	@Test
	public void getCarShouldReturnCarDetails() {
    	given(this.carService.schedulePickup(new Date(), new Route());)
        	.willReturn(new Date());

    	this.mvc.perform(get("/schedulePickup")
        	.accept(MediaType.JSON)
        	.andExpect(status().isOk());
	}
}

Keep Learning

Many of the frameworks and other capabilities mentioned in this best practices guide are described in the Spring Boot testing documentation. This recent video on testing messaging in Spring describes the use of Spock, JUnit, Mockito, Spring Cloud Stream and Spring Cloud Contract.

A more exhaustive tutorial is available to help you learn more about testing the web layer.