Spring Boot Testing best practices

General recommendations to keep in mind while writing spring boot tests

F.I.R.S.T Principles apply.

  1. F - Fast
  2. I - Independent
  3. R - Repeatable
  4. S - Self Validating
  5. T - Timely

Isolate the functionality to be tested by limiting the context of loaded frameworks/components.

Often times, it is sufficient to use jUnit without loading any additional frameworks. You only need to annotate your test with @Test

In the very naive code snipped below, there is 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 mockito if you have some interactions with 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 when testing spring boot applications.

@SpringBootTest annotation loads whole application, but it is better to limit Application Context only to a set of spring components that participate in test scenario, by listing them in 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 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);
    }
}

Running Database related tests gotchas.

Sometimes, Table Already Exist exceptions are thrown when testing with H2 database. This is an indication that H2 is not cleared between test invocations (because Application Context is Cached?). This behavior was observed when combining db tests with initialization of Wiremock. Also could 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 runs. Please 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

API/Controller tier testing.

Use @WebMvcTest to test rest APIs exposed through Controllers. Only list controllers that are being tested. Note: It looks like spring beans used by 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());
    }
}
Contactez-nous