Skip to main content

RestTestClient in Spring Framework 7: Testing REST APIs End-to-End (vs MockMvcTester)

RestTestClient is Spring Framework 7’s one fluent API across four binding modes: bare controller, MockMvc, full context, and a real server. What moved into two new Spring Boot 4 test modules, the TestRestTemplate migration path, and a real, verified surprise about what “real HTTP” hides from you.

Every Spring REST controller test you have ever written picked one of three APIs: MockMvc’s request builders, WebTestClient’s reactive-flavored fluent chain, or TestRestTemplate’s plain RestTemplate calls against a real port. Switching between a fast unit test, a validation-slice test, and a real end-to-end test usually meant switching APIs too β€” and relearning how each one reports a 400 or a missing header.

RestTestClient, new in Spring Framework 7, is one fluent API that works unchanged across all four levels of fidelity: a bare controller with no Spring context at all, a sliced MockMvc context, the full application context with no server, and a real embedded server on a real port. The same get()/post()/exchange()/expectStatus() chain reads identically whether you are testing a single method in isolation or hammering a real socket.

This article builds one small REST API and tests it four different ways with RestTestClient, then explains what actually changed underneath: two modularized Spring Boot 4 test artifacts you now need to add by hand, a genuinely separate (and still fully supported) migration path for existing TestRestTemplate tests, and one real surprise about what β€œreal HTTP” does and does not let you observe.

Versions used in this article. Spring Boot 4.1.1, Spring Framework 7.0.9 (GA 2025-11-13), JDK 25 (GraalVM CE 25.4.4.1.1), Maven 3.9. Every claim below was verified against the real jars on this exact classpath β€” javap output and real test runs, not reference-doc prose.

One client, four ways to wire it up

Every RestTestClient test starts the same way: call a static bindTo… factory method, then build(). What differs is what sits behind the client once you call exchange(). bindToController(Object…) instantiates the controller you hand it directly and dispatches to it with mock request and response objects β€” no Spring context, no handler-mapping lookup, nothing. bindTo(MockMvc) wraps an existing MockMvc β€” real dispatch through DispatcherServlet, still mock I/O. bindToApplicationContext(WebApplicationContext) does the same thing but discovers that MockMvc wiring from a full application context for you. bindToServer() is the only one that is not mock anything: it opens a real socket to a real embedded server on a real port.

get() / post() / exchange() / expectStatus() — the same four calls everywhere RestTestClient bindToController mock request/response bindTo(mockMvc) real dispatch, mock I/O bindToApplicationContext full context, mock I/O bindToServer real socket, real port no Spring context at all real Tomcat, real TCP fastest, most isolated slowest, most realistic The same test body — client.get().uri(…).exchange().expectStatus().isOk() — runs against every box above. What changes is how much of the real stack sits between that call and the response: nothing, MVC dispatch, a full context, or an actual connector. Each step buys realism and costs time — see the sections below.

Keep that ladder in mind: it is the one idea the rest of this article keeps coming back to, including a real surprise near the end about what “real HTTP” does and does not let you actually observe.

  • Full method list for every binding mode, verified against the real class files with javap: RestTestClient only exposes bindToController, bindToRouterFunction, bindToApplicationContext, bindTo(MockMvc), bindToServer() and bindToServer(ClientHttpRequestFactory) β€” there is no sixth option a reference doc’s prose might lead you to expect.
  • bindToRouterFunction exists for WebFlux’s functional endpoints and is out of scope here since this article’s API is a plain @RestController β€” see Spring’s functional endpoints reference.

The smallest thing that works: bindToController

Here is the whole API, stripped to one binding mode. No @SpringBootTest, no @WebMvcTest, no context of any kind β€” just a controller, a mock collaborator, and the fluent chain:

private final PersonService people = mock(PersonService.class);
private final RestTestClient client =
        RestTestClient.bindToController(new PersonController(people)).build();

@Test
void listsPeopleFromTheMockedService() {
    given(people.findAll()).willReturn(List.of(
            new Person(1L, "Jane Doe", "[email protected]"),
            new Person(2L, "Jason Pollack", "[email protected]")));

    client.get().uri("/api/persons")
            .exchange()
            .expectStatus().isOk()
            .expectBody()
            .jsonPath("$[0].name").isEqualTo("Jane Doe")
            .jsonPath("$[1].name").isEqualTo("Jason Pollack");
}

The full file, including the 404 case, is PersonControllerUnitTest.java. Run it and you get:

[INFO] Running com.ankurm.resttestclient.PersonControllerUnitTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.537 s -- in com.ankurm.resttestclient.PersonControllerUnitTest

β€” from the real 01-full-test-run-passing.txt. Half a second for the whole class, because there is nothing in it to start: no context, no servlet, no port. This is the binding mode to reach for by default, for the same reason a plain Mockito unit test is usually the right first test: it is the cheapest thing that can fail for the right reason.

  • The complete, javap-verified method list for RestTestClient and every nested spec interface (RequestHeadersSpec, RequestBodySpec, ResponseSpec, BodySpec, BodyContentSpec) is reproduced in full later in this article, where it matters for a real limitation β€” see “What RestTestClient still can’t do” below.
  • Reference: Spring Framework reference, RestTestClient.

What moved where: Spring Boot 4’s modularized testing support

RestTestClient itself needs nothing extra: it ships inside Spring Framework’s own spring-test jar, which spring-boot-starter-test already pulls in. Confirmed directly β€” unzip that jar and the class files are right there under org/springframework/test/web/servlet/client/. That is the one piece of good news in this section.

Everything else moved. TestRestTemplate, and the two annotations that wire either client into a @SpringBootTest context (@AutoConfigureRestTestClient, @AutoConfigureTestRestTemplate), are gone from spring-boot-test entirely and now live in a brand-new module: spring-boot-resttestclient. @WebMvcTest moved too, into its own new module, spring-boot-webmvc-test. Neither is pulled in by spring-boot-starter-test β€” you add both yourself.

Already on your classpath vs. what you now add yourself spring-test:7.0.9 RestTestClient pulled in by spring-boot-starter-test nothing to add for this one spring-boot-resttestclient:4.1.1 TestRestTemplate @AutoConfigureRestTestClient @AutoConfigureTestRestTemplate test-scope dependency you add by hand spring-boot-webmvc-test:4.1.1 @WebMvcTest also a test-scope dependency you add Same pattern as the main code: Boot 4 split testing support per web stack, alongside spring-boot-webmvc / spring-boot-webflux. Nothing here is removed — it moved, and the free auto-configuration moved with it. not the same artifact as spring-boot-restclient-test (that backs @RestClientTest instead)

Add both as test-scope dependencies:

<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-resttestclient</artifactId>
  <scope>test</scope>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-webmvc-test</artifactId>
  <scope>test</scope>
</dependency>

β€” the full, annotated version is in pom.xml.

Watch the spelling. spring-boot-resttestclient is not spring-boot-restclient-test. The second one is a real, separate artifact β€” it backs the older @RestClientTest slice, for testing code that calls out through RestClient/RestTemplate, which is a different feature from everything in this article. Both exist on Maven Central simultaneously, both are plausible autocomplete results, and only one of them has anything to do with RestTestClient.

There is a second, less obvious trap behind this one: even though the demo app here never calls RestTemplate directly, TestRestTemplate‘s own auto-configuration needs RestTemplateBuilder on the classpath to even inspect its bean method. Leave out spring-boot-starter-restclient and you get a NoClassDefFoundError, not a clean “bean not found” β€” see the TestRestTemplate section below for that failure captured directly.

  • Spring Boot’s own reference page for the full module split: Test Modules.
  • The same per-stack modularization pattern already hit the main (non-test) code in Boot 4 β€” see the companion post on Kotlin with Spring Boot 4.1 for another example of a class moving packages between 4.0 and 4.1.

Real dispatch, mocked I/O: the MockMvc slice

One layer up, a @WebMvcTest slice gives you the real Spring MVC machinery β€” argument resolvers, @Valid, exception handlers β€” without a running server. RestTestClient.bindTo(MockMvc) wraps the MockMvc that @WebMvcTest already injects, and notice what is not here: no @AutoConfigureRestTestClient, because nothing is asking Spring to create a RestTestClient bean. You are building the client yourself, around a bean the slice already gave you for free.

@WebMvcTest(PersonController.class)
class PersonMockMvcSliceTest {

    @Autowired
    MockMvc mockMvc;

    @MockitoBean
    PersonService people;

    RestTestClient client;

    @BeforeEach
    void setUp() {
        client = RestTestClient.bindTo(mockMvc).build();
    }

    @Test
    void aBlankNameFailsValidationBeforeTheServiceIsEverCalled() {
        client.post().uri("/api/persons")
                .contentType(MediaType.APPLICATION_JSON)
                .body(new Person(null, "", "not-an-email"))
                .exchange()
                .expectStatus().isBadRequest();
    }
}

Full file, including the successful-creation test and its Location header assertion: PersonMockMvcSliceTest.java.

[INFO] Running com.ankurm.resttestclient.PersonMockMvcSliceTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.259 s -- in com.ankurm.resttestclient.PersonMockMvcSliceTest

β€” from 01-full-test-run-passing.txt. @MockitoBean here is Spring Framework 7’s own replacement for the fully-removed @MockBean β€” covered in depth, with the real compiler failure, in the companion post on testing Spring Boot 4 without containers.

  • This is the binding mode to reach for for validation, security-filter and exception-handler behavior β€” anything that depends on real MVC dispatch but not on a real collaborator.
  • @WebMvcTest‘s own new home, spring-boot-webmvc-test, is covered above; nothing further is needed here beyond that one dependency.

The full context, no server: bindToApplicationContext and AssertJ

bindToApplicationContext goes one step further than the MockMvc slice: a real @SpringBootTest, the real PersonService bean, real validation, real exception handling β€” but webEnvironment defaults to MOCK, so there is still no open port. This is the same WebApplicationContext mechanism MockMvc has always used under a plain @SpringBootTest; RestTestClient just gives it the same fluent API as every other binding mode instead of MockMvc‘s request-builder syntax.

@SpringBootTest // webEnvironment defaults to MOCK: a WebApplicationContext, no server
class PersonApplicationContextTest {

    @Autowired
    WebApplicationContext context;

    RestTestClient client;

    @BeforeEach
    void setUp() {
        client = RestTestClient.bindToApplicationContext(context).build();
    }

    @Test
    void assertJStyleAssertionsWorkOnTheSameExchange() {
        RestTestClient.ResponseSpec spec = client.get().uri("/api/persons/1")
                .accept(MediaType.APPLICATION_JSON)
                .exchange();

        RestTestClientResponse response = RestTestClientResponse.from(spec);

        assertThat(response).hasStatusOk();
        assertThat(response).hasContentTypeCompatibleWith(MediaType.APPLICATION_JSON);
    }
}

Full file: PersonApplicationContextTest.java.

[INFO] Running com.ankurm.resttestclient.PersonApplicationContextTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.841 s -- in com.ankurm.resttestclient.PersonApplicationContextTest

β€” from 01-full-test-run-passing.txt. That second test is the other way to read a response: instead of the built-in expectStatus()/expectBody() chain, wrap the exchange in a RestTestClientResponse and keep writing ordinary AssertJ. One detail worth knowing before you go looking for it: the class is org.springframework.test.web.servlet.client.assertj.RestTestClientResponse β€” a sub-package, assertj, that the reference docs’ own prose does not call out.

  • You can start with the built-in workflow and switch to AssertJ at the end too, via returnResult() β€” both paths are shown in the reference docs: RestTestClient, AssertJ integration.
  • This is the right level for anything that genuinely needs the full context β€” cross-bean behavior, real transaction boundaries β€” but does not need a socket.

What breaks when you forget the annotation

Since neither test client is auto-configured any more, forgetting the annotation is the single most likely mistake β€” and, happily, it fails cleanly. Delete @AutoConfigureRestTestClient from the end-to-end test above and you get exactly what you would expect from any missing bean:

[ERROR] Tests run: 2, Failures: 0, Errors: 2, Skipped: 0, Time elapsed: 4.846 s <<< FAILURE! -- in com.ankurm.resttestclient.PersonEndToEndTest
[ERROR] com.ankurm.resttestclient.PersonEndToEndTest.theResponseIsCompressedOnTheWireButTheClientHidesIt -- Time elapsed: 0.016 s <<< ERROR!
org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.ankurm.resttestclient.PersonEndToEndTest': Unsatisfied dependency expressed through field 'client': No qualifying bean of type 'org.springframework.test.web.servlet.client.RestTestClient' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

β€” the full trace is in 03-resttestclient-not-autoconfigured.txt. A plain NoSuchBeanDefinitionException, exactly where @Autowired RestTestClient client sits. Nothing subtle: add the annotation back and the test passes again.

 

TestRestTemplate isn’t gone β€” it moved, and it’s no longer free

TestRestTemplate is a genuinely separate decision from RestTestClient, not a legacy fallback. If you have an existing suite of TestRestTemplate tests, the fix for Spring Boot 4 is one annotation, and the tests do not change at all:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureTestRestTemplate
class TestRestTemplateMigrationTest {

    @Autowired
    TestRestTemplate restTemplate;

    @Test
    void theOldApiStillWorksWithOneAnnotationAdded() {
        ResponseEntity<Person[]> response = restTemplate.getForEntity("/api/persons", Person[].class);

        assertThat(response.getStatusCode().is2xxSuccessful()).isTrue();
        assertThat(response.getBody()).hasSize(2);
    }
}

Full file: TestRestTemplateMigrationTest.java. Leave the annotation off and the failure has the identical shape to the one above, just for a different type:

org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'com.ankurm.resttestclient.TestRestTemplateMigrationTest': Unsatisfied dependency expressed through field 'restTemplate': No qualifying bean of type 'org.springframework.boot.resttestclient.TestRestTemplate' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {@org.springframework.beans.factory.annotation.Autowired(required=true)}

β€” full trace in 04-testresttemplate-not-autoconfigured.txt.

There is a second, much uglier way to get this wrong. Leave spring-boot-starter-restclient off the classpath entirely β€” easy to do, since this module’s own application code never calls RestTemplate β€” and the failure stops being a clean “bean not found”. TestRestTemplateTestAutoConfiguration‘s bean method needs RestTemplateBuilder just to be inspected by Spring’s condition evaluator, so its absence surfaces as a NoClassDefFoundError buried three levels under a BeanTypeDeductionException, long before the condition even gets a chance to decide anything: 02-testresttemplate-missing-restclient-dependency.txt. Same missing feature, two completely different-looking stack traces depending on which piece is actually absent.
  • TestRestTemplate also picked up a Kotlin extensions file (TestRestTemplateExtensionsKt) in the move β€” visible directly in the jar, not something either reference doc’s prose mentions.
  • Confirmed independently in the companion post on Kotlin with Spring Boot 4.1: TestRestTemplate is absent from Boot 4.1.1’s spring-boot-test jar. This article is the other half of that finding β€” where it actually went.

Real HTTP, but your client might still lie to you

Go back to the fidelity ladder from the start of this article. bindToServer is the one binding mode that opens a real socket, so it should be the one place you can trust what the wire actually did β€” for example, that Tomcat’s response compression really ran. It is also where this article’s one genuine surprise lives.

First, the ground truth. The app has server.compression enabled; curl straight against it proves the server really compresses the response:

$ curl -s -D - -o /dev/null http://localhost:18080/api/persons -H "Accept-Encoding: gzip"
HTTP/1.1 200
vary: accept-encoding
Content-Encoding: gzip
Content-Type: application/json
Transfer-Encoding: chunked

β€” from 05-curl-sees-real-gzip.txt. Now send the identical request through RestTestClient.bindToServer():

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureRestTestClient
class PersonEndToEndTest {

    @Autowired
    RestTestClient client;

    @Test
    void theResponseIsCompressedOnTheWireButTheClientHidesIt() {
        client.get().uri("/api/persons")
                .header("Accept-Encoding", "gzip")
                .exchange()
                .expectStatus().isOk()
                .expectHeader().valueEquals("Vary", "accept-encoding")
                .expectHeader().doesNotExist("Content-Encoding");
    }
}

That last assertion passes. Content-Encoding is not merely different β€” it is not there at all:

Same request, same compressed response, three different answers curl Accept-Encoding: gzip sees Content-Encoding: gzip — the real header, body still gzipped docs/output/05-curl-sees-real-gzip.txt RestClient SimpleClientHttpRequestFactory also sees Content-Encoding: gzip — HttpURLConnection does not decode it for you docs/output/07-simplefactory-shows-the-real-header.txt RestTestClient bindToServer() default no Content-Encoding header at all — java.net.http.HttpClient decoded it already docs/output/06-resttestclient-hides-the-gzip.txt transfer-encoding: chunked is the only tell left The only variable that changed is which ClientHttpRequestFactory answered. All three requests hit the identical compressed bytes on the wire; the difference is entirely client-side.

The reason is the same mechanism uncovered in the companion post on testing Spring Boot 4 without containers: Boot’s ClientHttpRequestFactoryBuilder.detect() picks whichever HTTP client implementation is on the classpath, and on this classpath that is JdkClientHttpRequestFactory β€” a thin wrapper around java.net.http.HttpClient. That client decodes gzip transparently and never hands the caller the header that said it happened. Swap in the older SimpleClientHttpRequestFactory (java.net.HttpURLConnection-based) against the exact same server and the header is right there:

@Test
void theOldHttpUrlConnectionFactoryShowsTheRealHeader() {
    RestClient client = RestClient.builder()
            .requestFactory(new SimpleClientHttpRequestFactory())
            .baseUrl("http://localhost:" + port)
            .build();

    String contentEncoding = client.get().uri("/api/persons")
            .header("Accept-Encoding", "gzip")
            .exchange((request, response) -> response.getHeaders().getFirst("Content-Encoding"));

    assertThat(contentEncoding).isEqualTo("gzip");
}

Full file: HttpClientChoiceComparisonTest.java; both runs are captured in 06-resttestclient-hides-the-gzip.txt and 07-simplefactory-shows-the-real-header.txt.

“Bind to a real server” buys you real HTTP, not necessarily real observability. If a test needs to assert on a response header that a client might normalize, decode, or otherwise quietly handle for you β€” compression is the common one, but caching validators and some redirect behavior can hit the same wall β€” check which ClientHttpRequestFactory actually answered before trusting what the assertion can see. The fidelity ladder from the start of this article has a hidden last rung: even “real HTTP” has an implementation detail standing between you and the wire.
  • One small, genuinely useful side-effect of all this: the Tomcat-emitted Vary header value is lowercase (accept-encoding), not the title-cased Accept-Encoding a reference example might lead you to assert against β€” caught the same way, by running the test and reading the real failure before fixing the assertion.
  • Spring Framework 7 also added an apiVersion(Object) hook on every RestTestClient request spec, tying into the framework’s new API versioning support β€” out of scope to demo here, but see the companion post on Spring Framework 7 API versioning if your API needs it.

What RestTestClient still can’t do

The reference docs’ prose does not mention a multipart gap, and a blog post claiming one is not something you can verify yourself without the repository open. The real class files are a better source. Here is every method on the two interfaces that would carry a multipart or file-upload builder if one existed:

$ javap -p 'org/springframework/test/web/servlet/client/RestTestClient$RequestHeadersSpec.class'
public interface org.springframework.test.web.servlet.client.RestTestClient$RequestHeadersSpec<S extends org.springframework.test.web.servlet.client.RestTestClient$RequestHeadersSpec<S>> {
  public abstract S accept(org.springframework.http.MediaType...);
  public abstract S acceptCharset(java.nio.charset.Charset...);
  public abstract S cookie(java.lang.String, java.lang.String);
  public abstract S cookies(java.util.function.Consumer<org.springframework.util.MultiValueMap<java.lang.String, java.lang.String>>);
  public abstract S ifModifiedSince(java.time.ZonedDateTime);
  public abstract S ifNoneMatch(java.lang.String...);
  public abstract S header(java.lang.String, java.lang.String...);
  public abstract S headers(java.util.function.Consumer<org.springframework.http.HttpHeaders>);
  public abstract S apiVersion(java.lang.Object);
  public abstract S attribute(java.lang.String, java.lang.Object);
  public abstract S attributes(java.util.function.Consumer<java.util.Map<java.lang.String, java.lang.Object>>);
  public abstract org.springframework.test.web.servlet.client.RestTestClient$ResponseSpec exchange();
  public abstract org.springframework.test.web.servlet.client.RestTestClient$ResponseSpec exchangeSuccessfully();
}

$ javap -p 'org/springframework/test/web/servlet/client/RestTestClient$RequestBodySpec.class'
public interface org.springframework.test.web.servlet.client.RestTestClient$RequestBodySpec extends org.springframework.test.web.servlet.client.RestTestClient$RequestHeadersSpec<org.springframework.test.web.servlet.client.RestTestClient$RequestBodySpec> {
  public abstract org.springframework.test.web.servlet.client.RestTestClient$RequestBodySpec contentLength(long);
  public abstract org.springframework.test.web.servlet.client.RestTestClient$RequestBodySpec contentType(org.springframework.http.MediaType);
  public abstract org.springframework.test.web.servlet.client.RestTestClient$RequestHeadersSpec<?> body(java.lang.Object);
}

β€” the full run, including the top-level static factories, is in 08-no-multipart-method-javap.txt. body(Object) is the only way in. There is no multipart() entry point and no MultiValueMap<String, Object>-based builder anywhere in the chain, the way WebTestClient has for its own multipart support. If your test suite needs to exercise a file-upload endpoint, that is still MockMvc‘s MockMultipartHttpServletRequestBuilder or MockMvcTester territory for now, not RestTestClient‘s β€” on the GA line this article runs on, Spring Framework 7.0.9.

Worth knowing before you plan around this gap: it is already closed upstream. spring-framework#35569 requested exactly this, and the fix landed with milestone 7.1.0-M1 β€” confirmed directly against Maven Central’s metadata for spring-test, which lists 7.1.0-M1 and 7.1.0-M2 as the newest releases past 7.0.9, with no 7.1.0 GA yet. So the honest version of this finding is narrower than “RestTestClient can’t do multipart”: it is “RestTestClient can’t do multipart on the Spring Framework 7.0.x line that is GA today,” and that gap is already scheduled to close whenever 7.1 ships.

Two smaller things worth knowing before you go looking for them, both visible in the method list above: exchangeSuccessfully() is a convenience that asserts a 2xx status as part of the exchange itself, and apiVersion(Object) is the hook into Spring Framework 7’s new API versioning feature β€” neither needed demonstrating at length here, but both are real, shipped methods, not speculative API surface.

RestTestClient vs MockMvcTester: picking one

Spring Framework 7 shipped two new fluent testing APIs in the same release, and they overlap enough that the natural question is which one to reach for. Nothing here was run against both tools side by side in this module β€” the companion repo is built around RestTestClient alone β€” so treat this section as a map of the documented differences rather than another verified-by-running-it exhibit, and read Dan Vega’s direct comparison if you want the primary source.

  MockMvcTester RestTestClient
Assertion style AssertJ’s assertThat() directly, no wrapper needed A fluent chain matching WebTestClient; AssertJ available via RestTestClientResponse.from(spec)
Which handler method ran Yes β€” can assert the request was dispatched to a specific controller method No equivalent
Multipart / file upload Supported Not on 7.0.9 GA, confirmed above by javap β€” already fixed for the 7.1 line via spring-framework#35569, closed at milestone 7.1.0-M1
Content types JSON-centric Broader HttpMessageConverter support β€” XML, Protobuf and others alongside JSON
Typed deserialization Less direct Straightforward, including typed lists via ParameterizedTypeReference
Real server (bindToServer) No equivalent β€” MockMvc never opens a socket Yes β€” the only one of the two that can reach a real running server

Where that leaves the decision: if your team already writes AssertJ-heavy MockMvc tests, tests mainly JSON, or needs file-upload coverage, MockMvcTester costs nothing to keep using. If you want one client whose API stays the same from a unit test through a full end-to-end run against a real port β€” which is the property this whole article has been demonstrating β€” RestTestClient is the one built for that. They are not mutually exclusive within one codebase; nothing stops a suite from using MockMvcTester for its JSON-and-multipart slice tests and RestTestClient for the end-to-end module.

Should you rewrite your existing TestRestTemplate or MockMvc tests to use RestTestClient? No. Nothing in Spring Boot 4 deprecates TestRestTemplate or MockMvc, and the migration path for the former is the one-annotation fix shown earlier in this article. Reach for RestTestClient for new test code, especially anything that needs to move between binding modes as it matures β€” a controller test today that becomes an end-to-end test next quarter changes nothing but its bindTo... call. Rewriting a passing suite for its own sake is not a reason; a suite that already works is not a bug.
  • The handler-method assertion that only MockMvcTester has comes from MvcTestResult‘s access to the underlying MvcResult β€” see the MockMvcTester reference for the full API.
  • Both tools are entirely orthogonal to @MockitoBean/@MockitoSpyBean, covered in the companion post on testing Spring Boot 4 without containers β€” mocking collaborators and choosing a test client are independent decisions.
Should you adopt RestTestClient at all right now? If you are starting a new Spring MVC project on Spring Boot 4.1 or later, yes β€” there is no reason to reach for MockMvc‘s request-builder syntax or WebTestClient on a non-reactive stack when one fluent API covers every fidelity level you will need, from a bare unit test to a real end-to-end run. If you have an existing, working suite of MockMvc or TestRestTemplate tests, there is no pressure to migrate them: nothing is deprecated, the one-annotation fix for TestRestTemplate covers the only breaking change that actually lands on you, and a passing test is not a problem to solve. The one case worth pausing on is a suite that leans on multipart or file-upload testing β€” RestTestClient cannot do that yet, and MockMvcTester or plain MockMvc still can.

Further reading

No Comments yet!

Leave a Reply

This site uses Akismet to reduce spam. Learn how your comment data is processed.