Skip to content

RestClient.Builder bean present in @SpringBootTest due to spring-boot-starter-webmvc-test, but missing at runtime without restclient starter #48253

@darenpang

Description

@darenpang

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:

  • 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)?
  1. 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.

Metadata

Metadata

Assignees

Labels

status: noteworthyA noteworthy issue to call out in the release notestype: bugA general bug

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions