[MM’s] Boot Notes — Spring Boot 4: The Next Generation of Testing in Action
The Next Generation: RestTestClient Unifies HTTP, Context Pausing Accelerates CI and Testcontainers Goes Zero-Config.

Testing in Spring has always evolved with the Java ecosystem — from XML-driven runners to annotation-based contexts. With Spring Framework 7 and Spring Boot 4, the testing model takes another leap forward. These releases retire JUnit 4, unify HTTP testing with a single client API, optimize context reuse and deeply integrate with Testcontainers (including the latest 2.x release) for infrastructure testing. The result: tests that are faster, cleaner, and ready for the modern JVM.
⚡ TL;DR (Quick Recap)
- JUnit 4 → JUnit 6
- RestTestClient: one fluent API for both MockMvc-backed and live-server tests, now with AssertJ support.
- Context pausing: Spring 7 suspends background threads between classes, significantly reducing suite execution time.
- Nested tests: consistent dependency injection at all levels.
- Prototype bean mocking: @MockitoBean finally supports non-singleton scopes.
- Application events, ServiceConnection …
Why Now
Spring 7 aligns with Jakarta EE 10, Java 21, and the new JUnit 6 architecture. The testing model that carried us through the last decade no longer fits today’s cloud-native, CI-driven pipelines. The Spring team used this release to clean up legacy layers, simplify APIs and ensure tests behave the same way in memory, on a container or in production.
Goodbye JUnit 4, Hello JUnit 6
For years, a Spring test looked like this:
@RunWith(SpringRunner.class)
public class MyTest {
@Autowired MyService service;
@Test
public void testSomething() { ... }
}
That runner is now deprecated. Spring 7 fully embraces JUnit 6’s API.
Why it matters
JUnit 6 isn’t a cosmetic upgrade — it’s a redesign that unlocks:
- Parameter injection for test and lifecycle methods
- Native support for @Nested tests
- Conditional execution (@EnabledOnOs, @EnabledIf)
- A clean extension model instead of runners/rules
If you’re still on JUnit 4, migration isn’t optional — but it’s straightforward, and the benefits are immediate.
RestTestClient: One API to Rule Them All
Testing HTTP endpoints used to mean choosing between three overlapping tools:
- MockMvc — fast but verbose
- TestRestTemplate — live server only
- WebTestClient — reactive only
RestTestClient in Spring Boot 4 replaces them with a unified API that works everywhere.
MockMvc-backed example:
@WebMvcTest(ProductApi.class)
@AutoConfigureRestTestClient
class ProductApiMockMvcTest {
@MockitoBean ProductService service;
@Autowired RestTestClient client;
@Test
void shouldGetProductById() {
given(service.getProductById(1L))
.willReturn(new Product(1L, "Laptop", 999.99));
var result = client.get()
.uri("/api/products/{id}", 1L)
.exchange()
.expectStatus().isOk()
.returnResult(Product.class);
assertThat(result.getResponseBody().name()).isEqualTo("Laptop");
verify(service).getProductById(1L);
}
}
Switch to a live server by changing one annotation:
@SpringBootTest(webEnvironment = RANDOM_PORT)
@AutoConfigureRestTestClient
class ProductApiLiveTest {
@Autowired RestTestClient client;
@Test
void shouldCreateProduct() {
var req = new CreateProductRequest("Laptop", 999.99);
var result = client.post()
.uri("/api/products")
.body(req)
.exchange()
.expectStatus().isCreated()
.returnResult(Product.class);
assertThat(result.getResponseBody()).isNotNull();
}
}
Same client, same API.
💡 Bonus: RestTestClient now speaks AssertJ fluently — no more Hamcrest boilerplate:
assertThat(result).hasStatusOk()
.hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON);
Context Pausing: Speed Through Efficiency
Running dozens of Spring tests often left background threads chewing CPU even when idle.
Spring 7 introduces automatic context pausing:
- A test class finishes.
- The shared ApplicationContext is paused – all SmartLifecycle beans stop.
- When the next test needs that context, it resumes instantly.
Opt-in is as simple as implementing isPauseable() → true in your SmartLifecycle components. Providing substantial performance improvements in test suites
Nested Tests That Actually Work
@Nested tests make suites readable, but injection in nested contexts used to be inconsistent.
Spring 7 fixes that — constructor, field, and parameter injection now work across all nesting levels:
@SpringBootTest
class ProductServiceNestedTest {
@Autowired ProductService service;
@Nested
class WhenProductExists {
Product product;
@BeforeEach
void create(@Autowired ProductService s) {
product = s.createProduct("Laptop", 999.99);
}
@Test
void shouldFindById(@Autowired ProductService s) {
assertThat(s.getProductById(product.id()).name())
.isEqualTo("Laptop");
}
}
}
Everything — @BeforeEach, @AfterEach, and @Test — now supports parameter injection reliably.
Mocking Prototype Beans with @MockitoBean
For years, @MockitoBean worked only for singleton beans.
Spring 7 removes that limitation:
@SpringBootTest
class PrototypeBeanTest {
@MockitoBean RequestIdGenerator generator; // prototype bean
@Autowired OrderService service;
@Test
void shouldMockPrototype() {
given(generator.generate()).willReturn("MOCK-ID");
var order = service.createOrder("CUST-1");
assertThat(order.requestId()).isEqualTo("MOCK-ID");
}
}
Behind the scenes, Spring treats the mock as a singleton for test scope — perfect for verifying interactions without touching real scope logic.
ApplicationEvents: Simpler Event-Driven Testing
No more manual listeners or wiring. Just annotate and inject:
@SpringBootTest
@RecordApplicationEvents
class OrderEventsTest {
@Autowired OrderService service;
@Test
void shouldPublishOrderCreated(ApplicationEvents events) {
var order = service.createOrder("CUST-1");
var created = events.stream(OrderCreatedEvent.class).findFirst().orElseThrow();
assertThat(created.orderId()).isEqualTo(order.id());
}
}
Each test method gets a fresh ApplicationEvents instance. It’s lightweight and perfectly scoped.
Testcontainers + @ServiceConnection = Zero Boilerplate
The @ServiceConnection annotation eliminates repetitive property plumbing also using a Testcontainers (including the latest 2.x release):
@SpringBootTest
@Testcontainers
class PostgresContainerTest {
@Container
@ServiceConnection
// org.testcontainers.postgresql.PostgreSQLContainer - Testcontainers 2.0
static PostgreSQLContainer postgres =
new PostgreSQLContainer("postgres:16-alpine");
@Autowired JdbcClient client;
@Test
void shouldConnect() {
assertThat(postgres.isRunning()).isTrue();
client.sql("SELECT 1").query(Integer.class).single();
}
}
Spring Boot detects the container and automatically configures the DataSource. It works out of the box for Postgres, MySQL, MongoDB, Redis, Kafka, RabbitMQ, and any container with a matching auto-configuration.
Final Takeaways
Spring Boot 4 and Spring Framework 7 mark a turning point in how we test Spring applications:
- Modern by default: JUnit 6 and Java 21 baseline.
- Unified testing API: RestTestClient for all web scenarios.
- Smarter execution: context pausing and consistent injection.
- Less boilerplate: ServiceConnection and MockitoBean enhancements.
If your project still runs JUnit 4 tests, plan your migration soon — the payoff is immediate: faster builds, cleaner code, and tests that mirror production behavior.
You can find all the code on GitHub.
Originally posted on marconak-matej.medium.com.