Skip to content
Spring Boot sb testing 3 min read

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 typeRolls back automatically?
@DataJpaTestYes — 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 @Transactional to roll back a RANDOM_PORT integration test. The request handling happens on a separate thread, outside your test’s transaction, so the writes are committed and @Transactional rolls 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);
}
Last updated June 13, 2026
Was this helpful?