@SpringBootTest bootstraps the full application context for integration testing — no mocking.
Since the whole context loads, you can @Autowire any bean picked up by component scanning.
It also creates a web environment, refined with the webEnvironment attribute:
MOCK(default) — loads a webApplicationContextwith a mock web environment.RANDOM_PORT— loads a realWebServerApplicationContext; the embedded server starts on a random port. This is the one to use for integration tests.DEFINED_PORT— loads a real web environment on a fixed port.NONE— loads anApplicationContextviaSpringApplicationwith no web environment.
Integration tests should ideally be kept separate from unit tests, and not run alongside them.
A simple controller integration test:
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)@AutoConfigureMockMvcpublic class EmployeeControllerITests {
@Autowired private MockMvc mockMvc;
@Autowired private EmployeeRepository employeeRepository;
@Autowired private ObjectMapper objectMapper;
@BeforeEach void setup() { employeeRepository.deleteAll(); }
@DisplayName("Integration Test Create Employee Controller") @Test void givenEmployeeObject_whenCreateEmployee_thenReturnSavedEmployee() throws Exception { Employee employee = Employee.builder() .firstName("travis") .lastName("scott") .email("travis@gmail.com") .build();
ResultActions response = mockMvc.perform(post("/api/employees") .contentType(MediaType.APPLICATION_JSON) .content(objectMapper.writeValueAsString(employee)));
response.andExpect(status().isCreated()) .andExpect(jsonPath("$.firstName", is(employee.getFirstName()))) .andExpect(jsonPath("$.lastName", is(employee.getLastName()))) .andExpect(jsonPath("$.email", is(employee.getEmail()))); }
@DisplayName("Integration Test for Get Employee By Id") @Test void givenEmployeeId_whenGetEmployeeById_thenReturnEmployee() throws Exception { Employee employee = Employee.builder() .firstName("Travis") .lastName("Scott") .email("travis@gmail.com") .build(); employeeRepository.save(employee);
ResultActions response = mockMvc.perform(get("/api/employees/{id}", employee.getId()));
response.andExpect(status().isOk()) .andDo(print()) .andExpect(jsonPath("$.firstName", is(employee.getFirstName()))) .andExpect(jsonPath("$.lastName", is(employee.getLastName()))) .andExpect(jsonPath("$.email", is(employee.getEmail()))); }}The problem with a real database
This works, but has drawbacks:
- You need an actual database installed to run the tests — an external dependency your test suite now relies on.
- Running the tests on a different machine means installing that database there too.
Testcontainers
Testcontainers is a Java library supporting JUnit tests with lightweight, throwaway instances of common databases, Selenium browsers, or anything else that runs in a Docker container. It lets you write self-contained integration tests that don’t depend on pre-installed external services — every run starts from a clean database, and the tests run on any machine with Docker.
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)@AutoConfigureMockMvc@TestContainersclass EmployerIT {
// TestContainer pulls the mysql image from Docker Hub, deploys it in a // container, and runs the integration tests against that instance @Container private static MySQLContainer mySQLContainer = new MySQLContainer("mysql:latest") .withUsername("username") .withPassword("password") .withDatabaseName("testdb");
@Autowired private MockMvc mockMvc;
@Autowired private EmployeeRepository employeeRepository;
@Autowired private ObjectMapper objectMapper;
@BeforeEach void setup() { employeeRepository.deleteAll(); }
// ... the same test methods as above (create, get all, get by id) work unchanged, // now running against the containerized MySQL instance instead of a local install}Singleton Containers pattern
With multiple integration test classes each using @Container, you’d spin up a fresh container
per test class — slow. The Singleton Containers pattern starts one container once, shared
across test classes: define an abstract base class owning the container, and have each test class
extend it.
public abstract class AbstractionBaseTest {
static final MySQLContainer MY_SQL_CONTAINER;
static { MY_SQL_CONTAINER = new MySQLContainer("mysql:latest") .withUsername("username") .withPassword("password") .withDatabaseName("ems");
MY_SQL_CONTAINER.start(); }
@DynamicPropertySource public static void dynamicPropertySource(DynamicPropertyRegistry registry) { registry.add("spring.datasource.url", MY_SQL_CONTAINER::getJdbcUrl); registry.add("spring.datasource.username", MY_SQL_CONTAINER::getUsername); registry.add("spring.datasource.password", MY_SQL_CONTAINER::getPassword); }}@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)@AutoConfigureMockMvcclass EmployeeIT extends AbstractionBaseTest {
@Autowired private MockMvc mockMvc;
@Autowired private EmployeeRepository employeeRepository;
@Autowired private ObjectMapper objectMapper;
@BeforeEach void setup() { employeeRepository.deleteAll(); }
// ... same test methods again, sharing the one running container across every // test class that extends AbstractionBaseTest}This pattern extends naturally to repository-layer tests too — just extend the same
AbstractionBaseTest.