Summary

After upgrading to Spring Boot 4.0.0 and adopting RestClient, I ran into a surprising discrepancy between test and runtime behavior:

At runtime (packaged jar / Docker), the application fails to start with NoSuchBeanDefinitionException: RestClient$Builder, as expected when spring-boot-starter-restclient is not on the main classpath.

In tests, however, @SpringBootTest + spring-boot-starter-webmvc-test successfully provides a RestClient.Builder bean, so all tests (including a “force initialize all beans” test) pass even when the runtime dependency is missing.

This means tests cannot detect a missing runtime dependency and effectively “mask” a configuration problem.

I would like to confirm if this asymmetry is expected, and if so, whether it could/should be documented or mitigated, because it makes it easy to ship a broken configuration even with seemingly thorough integration tests.

Environment

Spring Boot: 4.0.0 Java: 25.0.1 (Eclipse Temurin, running in Docker) Spring Web stack: Main scope: spring-boot-starter-webmvc (no spring-boot-starter-restclient) Test scope: spring-boot-starter-webmvc-test Build tool: Maven 3.9.11

Relevant dependencies Main (compile/runtime):

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webmvc</artifactId>
    </dependency>

    <!-- IMPORTANT: in the scenario that fails at runtime, there is NO restclient starter here -->
    <!--
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-restclient</artifactId>
    </dependency>
    -->
</dependencies>

Test:

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

mvn dependency:tree -Dscope=test shows:

[INFO] +- org.springframework.boot:spring-boot-starter-webmvc-test:jar:4.0.0:test
[INFO] |  +- org.springframework.boot:spring-boot-starter-jackson-test:jar:4.0.0:test
[INFO] |  +- org.springframework.boot:spring-boot-starter-test:jar:4.0.0:test
[INFO] |  |  ...
[INFO] |  +- org.springframework.boot:spring-boot-webmvc-test:jar:4.0.0:test
[INFO] |  |  \- org.springframework.boot:spring-boot-web-server:jar:4.0.0:compile
[INFO] |  \- org.springframework.boot:spring-boot-resttestclient:jar:4.0.0:test
[INFO] |     \- org.springframework.boot:spring-boot-restclient:jar:4.0.0:test
[INFO] |        \- org.springframework.boot:spring-boot-http-client:jar:4.0.0:test

So the test classpath includes spring-boot-restclient transitively, but the main classpath does not.

Configuration and code I have a configuration that defines a RestClient bean using a RestClient.Builder:

@Configuration
public class RouterManageConfig {

    @Bean
    RestClient asusRestClient(RouterProps props, RestClient.Builder builder) {
        return builder
                .baseUrl(props.baseUrl())
                .defaultHeader("X-Requested-With", "XMLHttpRequest")
                .build();
    }
}

The main application:

@SpringBootApplication
public class Main {
    public static void main(String[] args) {
        SpringApplication.run(Main.class, args);
    }
}

A very simple test:

@SpringBootTest
class MainTests {

    @Autowired
    ListableBeanFactory beanFactory;

    @Test
    void main() {
        assertThatNoException().isThrownBy(() -> Main.main(new String[0]));
    }

    @Test
    void printRestClientBuilderBeans() {
        var names = beanFactory.getBeanNamesForType(RestClient.Builder.class);
        System.out.println("=== RestClient.Builder beans ===");
        for (String name : names) {
            Object bean = beanFactory.getBean(name);
            System.out.println(name + " -> " + bean.getClass());
        }
    }
}

Test behavior (without spring-boot-starter-restclient) All tests passed. The output of printRestClientBuilderBeans() in tests is:

=== RestClient.Builder beans ===
restClientBuilder -> class org.springframework.web.client.DefaultRestClientBuilder

Runtime behavior (without spring-boot-starter-restclient) When I package the app and run it (either with java -jar or inside Docker), the application fails to start:

org.springframework.beans.factory.UnsatisfiedDependencyException:
  Error creating bean with name 'asusRouterClient' ...
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException:
  Error creating bean with name 'asusRestClient' ...
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException:
  No qualifying bean of type 'org.springframework.web.client.RestClient$Builder' available:
    expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

This matches my understanding: since spring-boot-restclient (or spring-boot-starter-restclient) is not present on the main classpath, RestClientAutoConfiguration does not run and no RestClient.Builder bean is created. Once I add spring-boot-starter-restclient to main dependencies and rebuild the image, the runtime error disappears.

Why I think this is a problem (or at least a sharp edge)

From a user perspective, I would expect my “full context” tests to be able to reveal misconfigurations like “I forgot to add the restclient starter”. In this case:

  • Runtime fails fast with NoSuchBeanDefinitionException (which is good).
  • Tests, however, pass entirely, even when I intentionally add a test to eagerly initialize all beans, because the test stack provides a RestClient.Builder bean that production does not have.

This makes it very easy to end up in a situation where:

  1. I upgrade to Boot 4, start using RestClient,
  2. I forget to add spring-boot-starter-restclient to main dependencies,
  3. My @SpringBootTest suite remains green (with spring-boot-starter-webmvc-test),
  4. Only the packaged app / Docker deployment fails.

I understand that “test classpath == runtime classpath” is generally not guaranteed, and that test starters intentionally add extra support. But in this specific case, the extra support hides a class of configuration problems instead of helping to detect them.

Questions / Suggestions

  1. Is this behavior intentional? That is, should users expect spring-boot-starter-webmvc-test to provide a RestClient.Builder even when the main app does not have spring-boot-starter-restclient?

  2. If it is intentional:

  3. Would it make sense to mention this in the 4.0 migration guide / documentation, with a note like:

“If you use RestClient in production, you must add spring-boot-starter-restclient (or spring-boot-restclient) to your main dependencies. Test starters such as spring-boot-starter-webmvc-test may provide a RestClient.Builder for tests even if the runtime dependency is missing.”

  • Or provide guidance on how to structure tests so that at least one set of tests uses exactly the runtime dependency set (e.g. a “minimal” test starter, or a recommendation to avoid certain test starters if you want strict parity)?

  • If it is not intentional:

  • Would it be possible to reconsider the dependency chain of spring-boot-starter-webmvc-test / spring-boot-resttestclient so that they don’t silently provide RestClient auto-configuration unless the main app also has the corresponding module?

  • Or to gate the RestClient.Builder auto-config behind some condition that keeps test and runtime behavior aligned?

Thanks for looking into this — I realize this might be “by design”, but from an upgrade/testability standpoint it felt surprising enough that I wanted to raise it.

Comment From: snicoll

Thanks for the report but that's a lot of text. Please consider moving the code to an actual project we can run as it'll be far more easier to make sure we're on the same page.

The starter does have indeed a dependency on spring-boot-resttestclient, but I am not sure that this reflects what we want to be happening here so it might just be an oversight of a previous refactoring. I'll flag this one to get some more feedback.

Comment From: darenpang

@snicoll Thank you for your reply . I’ve created a minimal sample project here: boot4-restclient-issue

Steps: - mvn test passes, including eager bean initialization. - mvn spring-boot:run (or java -jar) fails with No qualifying bean available for RestClient.Builder unless spring-boot-starter-restclient is added to main dependencies.

This shows the mismatch between test and runtime due to spring-boot-starter-webmvc-test -> resttestclient -> restclient.

Comment From: snicoll

Thanks. I meant going forward, a link to an example (as you did) is better than a very long description. I understand the problem and the mismatch. Once the team meets again we'll gather more context as why it's done this way (if it was intentional at all). We'll update then this issue.