Integration Testing
An integration test exercises your application the way a real client does — over real HTTP, through every layer, into a real database. Where slice tests verify one layer in isolation, integration tests verify that controllers, services, and repositories actually work together. The entry point is @SpringBootTest with a running server.
Starting a real server
webEnvironment = RANDOM_PORT boots the embedded server on a free port (avoiding clashes in CI) and auto-configures a TestRestTemplate pointed at it.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
class ProductApiIntegrationTest {
@Autowired TestRestTemplate rest;
@Autowired ProductRepository repository;
@LocalServerPort int port;
}
Because this is the full context, the ProductController, ProductService, and ProductRepository are all the real beans — nothing is mocked.
A full request-to-database test
This test POSTs a product over HTTP, asserts the HTTP response, and asserts that the row landed in the database.
@Test
void createsProductAndPersistsIt() {
var request = new ProductRequest("Keyboard", new BigDecimal("49.90"));
ResponseEntity<Product> response =
rest.postForEntity("/api/products", request, Product.class);
// 1. Assert the HTTP response
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CREATED);
assertThat(response.getBody().id()).isNotNull();
// 2. Assert persistence actually happened
assertThat(repository.findById(response.getBody().id()))
.isPresent()
.get()
.extracting(Product::getName)
.isEqualTo("Keyboard");
}
Output:
ProductApiIntegrationTest > createsProductAndPersistsIt() PASSED
BUILD SUCCESS
Seeding data before a test
Arrange the database via the repository (or a SQL script) so the endpoint has something to return.
@Test
void listsExistingProducts() {
repository.saveAll(List.of(
new Product("A", new BigDecimal("10")),
new Product("B", new BigDecimal("20"))));
ResponseEntity<Product[]> response =
rest.getForEntity("/api/products", Product[].class);
assertThat(response.getBody()).hasSize(2);
}
For SQL-driven setup, @Sql runs a script before (or after) the test method:
@Test
@Sql("/seed-products.sql")
void readsSeededData() { /* ... */ }
A note on rollback
Here lies the most common integration-test surprise: @SpringBootTest does NOT roll back by default. Slice tests like @DataJpaTest are transactional and roll back automatically, but a full integration test with a running server is not — and adding @Transactional to a RANDOM_PORT test does not help, because the HTTP request runs on a different thread with its own transaction.
| Test type | Rolls back automatically? |
|---|---|
@DataJpaTest | Yes — wrapped in a transaction |
@SpringBootTest + @Transactional (no real port / MOCK) | Yes |
@SpringBootTest(RANDOM_PORT) | No — server runs on another thread |
So for RANDOM_PORT tests you must clean state yourself. The simplest reliable approach is to clear the relevant tables after each test:
@AfterEach
void cleanUp() {
repository.deleteAll();
}
Warning: Do not rely on
@Transactionalto roll back aRANDOM_PORTintegration test. The request handling happens on a separate thread, outside your test’s transaction, so the writes are committed and@Transactionalrolls back nothing meaningful. Clean up explicitly or use a fresh database per run.
Keeping integration tests realistic
For high fidelity, run against the same database engine you use in production rather than an embedded H2. Testcontainers spins up a disposable Postgres (or MySQL, Mongo, Kafka) in Docker and, with @ServiceConnection, wires Spring’s DataSource to it automatically.
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
@Testcontainers
class RealDbIntegrationTest {
@Container
@ServiceConnection
static PostgreSQLContainer<?> postgres = new PostgreSQLContainer<>("postgres:16");
@Autowired TestRestTemplate rest;
// tests run against a real Postgres in Docker
}
Tip: Keep integration tests few and high-value — happy paths and critical flows. They are slower than unit and slice tests, so let the lower layers of the testing pyramid carry the bulk of coverage.
Asserting error responses
TestRestTemplate does not throw on non-2xx, so you can assert error bodies directly.
@Test
void returns404ForMissingProduct() {
ResponseEntity<String> response =
rest.getForEntity("/api/products/9999", String.class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.NOT_FOUND);
}