Skip to content
thesarfo

Concept

Testing: Integration Tests

Full-context integration testing with @SpringBootTest, the WebEnvironment options, and using Testcontainers (plus the Singleton Containers pattern) to avoid depending on a pre-installed database.

views 0

@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 web ApplicationContext with a mock web environment.
  • RANDOM_PORT — loads a real WebServerApplicationContext; 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 an ApplicationContext via SpringApplication with 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)
@AutoConfigureMockMvc
public 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
@TestContainers
class 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)
@AutoConfigureMockMvc
class 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.