[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.

Spring Boot 4: The Next Generation of Testing in Action

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:

  1. A test class finishes.
  2. The shared ApplicationContext is paused – all SmartLifecycle beans stop.
  3. 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.