Spring @RestClientTest: Testing REST Clients in Spring Boot 3.2 Like a Senior Dev
Master Spring Boot @RestClientTest with production-tested patterns.
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
- ✓Java 17 or later (Spring Boot 3.x requires it)
- ✓Spring Boot 3.2+ project with spring-boot-starter-web dependency
- ✓Basic understanding of RestClient API and dependency injection
- ✓Familiarity with JUnit 5 and Mockito
• @RestClientTest is a focused test slice for testing RestClient-based REST calls in Spring Boot 3.x • It auto-configures a MockRestServiceServer to stub HTTP interactions without real network calls • Use it with @Autowired RestClient.Builder and @Inject Beans to test client logic in isolation • Avoid common pitfalls like forgetting to reset mocks or testing against production endpoints • Combine with WireMock or Testcontainers for contract testing in CI/CD pipelines
Think of @RestClientTest as a driving simulator for your code that talks to external APIs. Instead of actually driving on real roads (making real HTTP calls) and risking accidents (network failures, rate limits), you use a simulator that perfectly mimics the traffic lights and road signs. Your code thinks it's talking to a real server, but you control every response to test how it handles green lights, red lights, and sudden road closures.
| Chrome | Firefox | Safari | Edge |
|---|---|---|---|
| ✓ | ✓ | ✓ | ✓ |
If you've been writing Spring Boot applications for any length of time, you've probably used RestTemplate or the newer RestClient to make HTTP calls to external services. And if you've written tests for those clients, you know the pain: flaky tests that fail because an external API is down, tests that take 30 seconds because of network latency, or worse, tests that accidentally hit production endpoints. I've seen a payment-processing service send $50,000 in test charges to a real Stripe account because someone forgot to mock the HTTP call. Don't be that person.
Spring Boot 3.2 introduced @RestClientTest, a focused test slice that auto-configures a MockRestServiceServer to intercept HTTP calls made by RestClient. It's the spiritual successor to @RestClientTest for RestTemplate (yes, that existed in older versions) but built for the modern, non-blocking RestClient API. This annotation gives you a clean, isolated environment to test your REST client logic without spinning up a full application context or hitting real networks.
In this tutorial, we'll cover how to use @RestClientTest effectively, what the official docs gloss over, and how I've used it in production SaaS billing systems to catch bugs before they hit customers. We'll write real code, discuss common mistakes that have cost teams hours of debugging, and give you patterns that scale from a single microservice to a distributed system with 50+ API dependencies.
Setting Up @RestClientTest: The Basics
Let's start with a concrete example. Imagine you're building a SaaS billing service that needs to call an external payment gateway API to charge customers. You have a PaymentClient class that uses RestClient to send POST requests to the gateway. Here's how you test it with @RestClientTest.
First, add the dependency to your build.gradle or pom.xml. For Maven, you need spring-boot-starter-test which includes MockRestServiceServer. The annotation itself is part of spring-boot-test-autoconfigure.
Now create your test class. The key is to annotate it with @RestClientTest and specify the component(s) you want to test. Spring Boot will create a sliced context with only the beans necessary for RestClient testing: a MockRestServiceServer, a RestClient.Builder, and any beans you explicitly reference.
Notice we use @Autowired for both the MockRestServiceServer and the PaymentClient. The server intercepts all HTTP calls made by the RestClient created via the injected RestClient.Builder. We then define expectations using MockRestServiceServer's fluent API: expect a POST request to the payment endpoint, respond with a 200 status and a JSON body.
The test verifies that our client correctly extracts the transaction ID from the response. This is fast, deterministic, and doesn't touch the network.
verify() to enforce all expectations were met.What the Official Docs Won't Tell You
The Spring Boot documentation covers the happy path: one client, one mock, one test. But in production, you'll face real-world complexities. Here's what I've learned from running a SaaS platform that handles millions of API calls daily.
First, @RestClientTest only auto-configures MockRestServiceServer if you use RestClient.Builder. If you inject a fully built RestClient (e.g., via @Autowired RestClient), the mock won't intercept calls. This is because MockRestServiceServer wraps the ClientHttpRequestFactory inside the builder. If you create your RestClient manually, you must set up the mock yourself.
Second, the test slice does not load your entire application context. That's usually a good thing for speed, but it means any beans your client depends on (e.g., configuration properties, other services) must be explicitly imported or mocked. Use @Import(MyConfig.class) or @MockitoBean for dependencies.
Third, MockRestServiceServer works with synchronous RestClient calls only. If you're using the new reactive WebClient, you need @WebFluxTest or WireMock. Don't try to force RestClient into async scenarios—it's synchronous by design.
Finally, the official docs don't emphasize that you can test error scenarios like 4xx and 5xx responses. This is critical for building resilient clients that handle retries and fallbacks. Let's see that next.
Testing Complex Request/Response Patterns
Real-world REST clients don't just send simple JSON payloads. They deal with headers, query parameters, multipart forms, and authentication tokens. @RestClientTest can handle all of these, but you need to know the right matchers.
For headers, use header(String name, String... values). For query parameters, use queryParam(String name, String... values). If you're dealing with JWT tokens in Authorization headers, you can match exact values or use Matchers.startsWith().
Multipart requests are trickier. MockRestServiceServer doesn't natively support multipart content matching. In that case, I recommend using WireMock as a supplementary tool. But for most JSON-based APIs, the built-in matchers are sufficient.
Another common pattern is testing clients that parse paginated responses. You can stub multiple pages by defining multiple expectations with different URIs (e.g., /api/users?page=1, /api/users?page=2).
Let's look at a client that sends an API key in the header and expects a paginated list of invoices.
Handling Authentication and OAuth2 Tokens
Most production REST clients need authentication. Whether it's a static API key, Basic Auth, or OAuth2 bearer tokens, you need to test that your client sends the correct credentials. @RestClientTest can handle this, but there's a nuance: if your client obtains tokens dynamically (e.g., via a client credentials grant), you need to mock that token acquisition too.
For static API keys, simply match the header in your mock expectations. For OAuth2, you have two options: stub the token endpoint separately, or use a fixed token and mock the interceptor. I prefer the latter for unit tests—it's simpler and faster.
Spring Boot's RestClient.Builder allows you to add request interceptors. If your client uses an interceptor to inject a bearer token, you can test that interceptor separately or inject a mock token provider.
Let's see an example where the client fetches an OAuth2 token from an auth server before calling the actual API.
Testing Timeouts and Connection Errors
Network failures are inevitable. Your REST client must handle timeouts, connection refused, and DNS resolution failures gracefully. MockRestServiceServer can simulate these by throwing exceptions or using custom responses.
For timeouts, you can use withSuccess() with a delay, but that's not ideal because the test will wait. Instead, use withException() to throw a ResourceAccessException or SocketTimeoutException. This tests your client's error handling without slowing down the test suite.
For connection refused, you can throw a ConnectException. The key is to ensure your client doesn't crash or leak resources. In a production billing system, a timeout shouldn't cause a double charge or a lost transaction.
Let's write a test that verifies the client throws a custom exception on timeout, and that the retry mechanism doesn't retry on certain errors.
Integrating with WireMock for Advanced Scenarios
While MockRestServiceServer is great for unit tests, sometimes you need more control. WireMock is a popular HTTP mock server that runs on a real port and supports advanced features like stateful behavior, response templating, and recording/proxying.
When should you use WireMock over @RestClientTest? Use WireMock when: you need to test the actual HTTP client configuration (e.g., SSL, proxy), you're doing contract testing with a shared stub repository, or you need to simulate complex scenarios like delayed responses or chunked encoding.
Spring Boot's @RestClientTest doesn't integrate with WireMock out of the box, but you can configure it easily. Create a WireMock server in a @BeforeEach, point your RestClient to its URL, and define stubs. This gives you the best of both worlds: the speed of a test slice with the power of WireMock.
Here's how to set up a hybrid test that uses WireMock for a multipart upload scenario.
Performance Optimization: Keeping Tests Fast
One of the main reasons to use @RestClientTest is speed. A typical @RestClientTest test runs in under 100ms, compared to 5-10 seconds for a full @SpringBootTest. But if you're not careful, your test suite can still slow down.
First, avoid @SpringBootTest for REST client tests. I've seen teams use @SpringBootTest(classes = {PaymentClient.class, RestClientConfig.class}) which still loads a large context. Use @RestClientTest instead—it only loads the web client slice.
Second, limit the number of beans you import. If your client depends on a service that loads a database connection, mock that service with @MockBean. Don't let your test accidentally connect to a database.
Third, use static imports for MockRestServiceServer matchers to reduce boilerplate. IntelliJ can auto-import them.
Fourth, parallelize your tests. Since @RestClientTest tests are isolated (no shared state), you can run them in parallel with JUnit 5's @Execution(ExecutionMode.CONCURRENT). This can cut your test time by 50% or more.
Let's see a benchmark comparison.
Debugging Failures: A Production Debug Guide
Even with great tests, things fail. When a @RestClientTest test fails in CI, you need to debug quickly. Here's a systematic approach I've developed over years.
First, check the mock expectations. Did you define the correct URL, method, headers, and body? A common mistake is using requestTo() with an absolute URL when the client uses a relative path with a base URL. Use requestToUriTemplate() or match the full URL.
Second, verify the order of expectations. MockRestServiceServer matches expectations in the order they are defined. If your client makes calls in a different order, the test fails. Use server.verify() to see which expectations were not matched.
Third, enable debug logging. Add logging.level.org.springframework.test.web.client=DEBUG to your test properties. This prints every request that goes through the mock server, including unmatched ones.
Fourth, use a custom RequestMatcher for complex scenarios. If the built-in matchers don't work, implement RequestMatcher interface to inspect the request manually.
Here's a debug guide table for common symptoms.
server.verify() to pinpoint mismatches.The $50,000 Mistake: When Tests Hit Production Stripe
- Never assume test profiles automatically mock external services—explicitly verify every HTTP call is stubbed.
- Use @RestClientTest or @WebMvcTest instead of @SpringBootTest when testing REST clients to avoid unintended real network calls.
- Add a CI pipeline step that scans for @SpringBootTest annotations and flags them for review.
server.verify() to identify which expectation was not matched.logging.level.org.springframework.test.web.client=DEBUGserver.verify() in @AfterEach| File | Command / Code | Purpose |
|---|---|---|
| PaymentClientTest.java | @RestClientTest(PaymentClient.class) | Setting Up @RestClientTest |
| PaymentClientErrorTest.java | @RestClientTest(PaymentClient.class) | What the Official Docs Won't Tell You |
| InvoiceClientPaginationTest.java | @RestClientTest(InvoiceClient.class) | Testing Complex Request/Response Patterns |
| AuthenticatedClientTest.java | @RestClientTest(AuthenticatedClient.class) | Handling Authentication and OAuth2 Tokens |
| TimeoutHandlingTest.java | @RestClientTest(PaymentClient.class) | Testing Timeouts and Connection Errors |
| FileUploadClientWireMockTest.java | @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE) | Integrating with WireMock for Advanced Scenarios |
| BenchmarkComparison.java | @RestClientTest(PaymentClient.class) | Performance Optimization |
| CustomRequestMatcher.java | public class CustomHeaderMatcher implements RequestMatcher { | Debugging Failures |
Key takeaways
server.verify() to enforce all expectations were met.Interview Questions on This Topic
What is @RestClientTest and when would you use it over @SpringBootTest?
Frequently Asked Questions
20+ years shipping production Java in banking & fintech. Lessons pulled from things that broke in production.
That's Spring Boot. Mark it forged?
5 min read · try the examples if you haven't