Best Practices for How to Test Spring Boot Applications
Version Note
This guide was written for Spring Boot 2.6.x. Your results might vary with other versions.
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 static org.junit.Assert.assertEquals;
import org.junit.Test;
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 static org.mockito.Matchers.any;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import java.util.Date;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
import org.mockito.runners.MockitoJUnitRunner;
@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 static org.junit.Assert.assertTrue;
import java.util.Date;
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;
@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);
}
}
Running database-related tests
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.
A more exhaustive tutorial is available to help you learn more about testing the web layer.
Give Feedback
Help us improve by sharing your thoughts.