@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.
@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
webEnvironment | What starts | Use it with |
|---|---|---|
MOCK (default) | A web context, no server | @AutoConfigureMockMvc and MockMvcTester |
RANDOM_PORT | Embedded Tomcat on a free port | @AutoConfigureRestTestClient and RestTestClient |
DEFINED_PORT | Tomcat on server.port | Rarely; ports clash on CI machines |
NONE | No web context | Batch 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
@SpringBootTestfor tests a slice or a unit test would cover. - Relying on an
application.propertiesthat points at a shared development database. Give tests their own database, ideally a container. - Calling
@DirtiesContextto fix leaking state. It throws the cached context away and the next class starts a new one.
Example
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) {
}
(proxyBeanMethods = false)
class PostgresContainerConfig {
PostgreSQLContainer postgres() {
return new PostgreSQLContainer("postgres:18-alpine");
}
}
(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
(PostgresContainerConfig.class)
class OrderApiIT {
RestTestClient client;
JdbcClient jdbc;
void deleteOrders() {
// The server committed its own transaction, so nothing rolls back by itself.
jdbc.sql("delete from purchase_orders").update();
}
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"));
}
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.
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.
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.
# 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