-
Notifications
You must be signed in to change notification settings - Fork 41.7k
Description
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:
- I upgrade to Boot 4, start using RestClient,
- I forget to add spring-boot-starter-restclient to main dependencies,
- My
@SpringBootTestsuite remains green (with spring-boot-starter-webmvc-test), - 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
-
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? -
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)?
- 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.