@SpringBootTest

Start the whole application in a test, with a mock web environment or a real server on a random port, and know what that costs.

Syntax@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @AutoConfigureRestTestClient class ApiIT { @Autowired RestTestClient client; } @SpringBootTest @AutoConfigureMockMvc // MOCK environment, inject MockMvcTester

@SpringBootTest (from org.springframework.boot.test.context) starts your application the way SpringApplication.run would: it finds the @SpringBootApplication class by searching up the package tree, loads every bean and applies all auto-configuration. It is the right tool for checking that the pieces fit together: configuration, security, transactions, JSON and the database in one request.

The web environment

webEnvironmentWhat startsUse it with
MOCK (default)A web context, no server@AutoConfigureMockMvc and MockMvcTester
RANDOM_PORTEmbedded Tomcat on a free port@AutoConfigureRestTestClient and RestTestClient
DEFINED_PORTTomcat on server.portRarely; ports clash on CI machines
NONENo web contextBatch jobs, message listeners

With MOCK, requests go through MockMvc on the test thread. With RANDOM_PORT, real HTTP requests reach a real server, so servlet filters, the error page, compression and connection handling all behave as in production. RestTestClient (from org.springframework.test.web.servlet.client) is the Spring Framework 7 client for these tests; @AutoConfigureRestTestClient binds it to the running server for you.

Transactions do not roll back across a real server

@Transactional on a test method rolls back work done on the test thread. In a MOCK test the controller runs on that thread, so the rollback covers it. With RANDOM_PORT the request is handled on a server thread with its own transaction, which commits. Rows pile up between tests unless you delete them, for example in an @AfterEach method.

Speed

Starting the full context takes seconds. The TestContext framework caches it and reuses it for every test class with the same configuration, so the real cost is the number of distinct configurations. Each different properties value, profile or set of mocked beans starts another context, and old contexts keep their connection pools open until they are evicted from the cache.

Common mistakes

  • Using @SpringBootTest for tests a slice or a unit test would cover.
  • Relying on an application.properties that points at a shared development database. Give tests their own database, ideally a container.
  • Calling @DirtiesContext to fix leaking state. It throws the cached context away and the next class starts a new one.

Example

Example · java
import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureRestTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.TestConfiguration;
import org.springframework.boot.testcontainers.service.connection.ServiceConnection;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.test.web.servlet.client.RestTestClient;
import org.testcontainers.postgresql.PostgreSQLContainer;

record CreateOrder(String sku, int quantity) {
}

record OrderView(long id, String sku, int quantity, String status) {
}

@TestConfiguration(proxyBeanMethods = false)
class PostgresContainerConfig {

    @Bean
    @ServiceConnection
    PostgreSQLContainer postgres() {
        return new PostgreSQLContainer("postgres:18-alpine");
    }
}

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
@Import(PostgresContainerConfig.class)
class OrderApiIT {

    @Autowired
    RestTestClient client;

    @Autowired
    JdbcClient jdbc;

    @AfterEach
    void deleteOrders() {
        // The server committed its own transaction, so nothing rolls back by itself.
        jdbc.sql("delete from purchase_orders").update();
    }

    @Test
    void createsAnOrderAndReadsItBack() {
        OrderView created = client.post().uri("/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .body(new CreateOrder("SKU-1", 2))
                .exchange()
                .expectStatus().isCreated()
                .expectHeader().exists("Location")
                .expectBody(OrderView.class)
                .returnResult()
                .getResponseBody();

        assertThat(created).isNotNull();

        client.get().uri("/orders/{id}", created.id())
                .exchange()
                .expectStatus().isOk()
                .expectBody(OrderView.class)
                .value(order -> assertThat(order.status()).isEqualTo("NEW"));
    }

    @Test
    void returnsTheDefaultErrorBodyForUnknownOrders() {
        client.get().uri("/orders/{id}", 999_999)
                .exchange()
                .expectStatus().isNotFound()
                .expectBody()
                .jsonPath("$.status").isEqualTo(404)
                .jsonPath("$.path").isEqualTo("/orders/999999");
    }
}

When to use it

  • Check a full order flow end to end: JSON in, validation, service, PostgreSQL, JSON out, through a real HTTP server.
  • Verify that the default error response for an unknown URL has the shape the API documentation promises.
  • Prove that configuration properties are bound and used, for example an invoice prefix per environment.

More examples

The mock environment with automatic rollback

In the default <code>MOCK</code> environment the controller, the service and the repository all run on the test thread, inside the test's transaction. Spring rolls it back after the method, so no clean-up is needed and the next test starts from the same data. The <code>JdbcClient</code> query sees the uncommitted row because it uses the same transaction.

Example · java
import static org.assertj.core.api.Assertions.assertThat;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.webmvc.test.autoconfigure.AutoConfigureMockMvc;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.test.web.servlet.assertj.MockMvcTester;
import org.springframework.transaction.annotation.Transactional;

@SpringBootTest
@AutoConfigureMockMvc
@Transactional
class OrderFlowIT {

    @Autowired
    MockMvcTester mvc;

    @Autowired
    JdbcClient jdbc;

    @Test
    void storesTheOrderInTheDatabase() {
        assertThat(mvc.post().uri("/orders")
                .contentType(MediaType.APPLICATION_JSON)
                .content("""
                        {"sku": "SKU-9", "quantity": 1}
                        """))
                .hasStatus(HttpStatus.CREATED);

        Integer count = jdbc.sql("select count(*) from purchase_orders where sku = :sku")
                .param("sku", "SKU-9")
                .query(Integer.class)
                .single();
        assertThat(count).isEqualTo(1);
    }
}

Overriding a property and a bean for one class

<code>properties</code> adds or overrides configuration for this test class only. <code>@TestBean</code> (from <code>org.springframework.test.context.bean.override.convention</code>) replaces the application's <code>Clock</code> bean with the value returned by a static factory method of the same name. Both change the context's cache key, so this class gets its own context; group tests that need the same overrides into one class.

Example · java
import static org.assertj.core.api.Assertions.assertThat;

import java.time.Clock;
import java.time.Instant;
import java.time.ZoneOffset;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.stereotype.Service;
import org.springframework.test.context.bean.override.convention.TestBean;

@Service
class InvoiceNumbers {

    private final Clock clock;
    private final String prefix;

    InvoiceNumbers(Clock clock, @Value("${app.invoices.prefix}") String prefix) {
        this.clock = clock;
        this.prefix = prefix;
    }

    String next(long sequence) {
        int year = clock.instant().atZone(ZoneOffset.UTC).getYear();
        return "%s-%d-%06d".formatted(prefix, year, sequence);
    }
}

@SpringBootTest(properties = "app.invoices.prefix=TEST")
class InvoiceNumbersIT {

    @TestBean
    Clock clock;

    static Clock clock() {
        return Clock.fixed(Instant.parse("2027-01-01T00:00:00Z"), ZoneOffset.UTC);
    }

    @Autowired
    InvoiceNumbers numbers;

    @Test
    void usesThePrefixAndTheCurrentYear() {
        assertThat(numbers.next(42)).isEqualTo("TEST-2027-000042");
    }
}

Test configuration that keeps contexts cheap

A profile file activated by the same <code>@ActiveProfiles("test")</code> in every test adds its values on top of the main <code>application.properties</code>, so shared test settings live in one place and do not fragment the context cache. Do not name it <code>application.properties</code>: a file with that name in <code>src/test/resources</code> shadows the main one on the classpath, and every main setting silently disappears from the tests. The cache logger prints statistics such as hit and miss counts; a miss on almost every test class means too many distinct configurations. <code>validate</code> makes Hibernate check the mapping against the migrated schema rather than quietly creating tables that production will not have.

Example · properties
# src/test/resources/application-test.properties
# Loaded on top of the main file by @ActiveProfiles("test") on a shared base class.
# Fail fast if the schema and the entities disagree, instead of altering tables.
spring.jpa.hibernate.ddl-auto=validate
spring.main.banner-mode=off

# Log each context the TestContext framework creates or reuses.
logging.level.org.springframework.test.context.cache=debug

# Do not schedule background jobs while tests run.
app.scheduling.enabled=false

Discussion

  • Be the first to comment on this lesson.