Add rest-test-client module: RestTestClient across all four binding modes, TestRestTemplate relocation, gzip-hiding HTTP client surprise (post #44)

This commit is contained in:
Claude
2026-10-08 09:40:13 +00:00
parent 5851521420
commit 5e4d481c02
22 changed files with 795 additions and 5 deletions
@@ -0,0 +1,15 @@
$ mvn clean test
[INFO] Running com.ankurm.resttestclient.HttpClientChoiceComparisonTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.285 s -- in com.ankurm.resttestclient.HttpClientChoiceComparisonTest
[INFO] Running com.ankurm.resttestclient.PersonEndToEndTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 1.177 s -- in com.ankurm.resttestclient.PersonEndToEndTest
[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
[INFO] Running com.ankurm.resttestclient.TestRestTemplateMigrationTest
[INFO] Tests run: 1, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 0.802 s -- in com.ankurm.resttestclient.TestRestTemplateMigrationTest
[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
[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
[INFO] Tests run: 11, Failures: 0, Errors: 0, Skipped: 0
[INFO] BUILD SUCCESS
@@ -0,0 +1,56 @@
$ mvn test -Dtest=TestRestTemplateMigrationTest
# captured with spring-boot-starter-restclient removed from pom.xml: @AutoConfigureTestRestTemplate
# is present, but TestRestTemplateTestAutoConfiguration itself cannot even be inspected, because
# TestRestTemplateBuilder's bean method signature needs org.springframework.boot.restclient.RestTemplateBuilder
# on the classpath, and nothing else in this module pulls that class in.
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 0.401 s <<< FAILURE! -- in com.ankurm.resttestclient.TestRestTemplateMigrationTest
[ERROR] com.ankurm.resttestclient.TestRestTemplateMigrationTest.theOldApiStillWorksWithOneAnnotationAdded -- Time elapsed: 0.008 s <<< ERROR!
java.lang.IllegalStateException: Failed to load ApplicationContext for [WebMergedContextConfiguration@4b2acb12 testClass = com.ankurm.resttestclient.TestRestTemplateMigrationTest, ...]
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.lambda$loadContext$0(DefaultCacheAwareContextLoaderDelegate.java:195)
at org.springframework.test.context.cache.DefaultContextCache.put(DefaultContextCache.java:214)
at org.springframework.test.context.cache.DefaultCacheAwareContextLoaderDelegate.loadContext(DefaultCacheAwareContextLoaderDelegate.java:160)
at org.springframework.test.context.support.DefaultTestContext.getApplicationContext(DefaultTestContext.java:128)
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:156)
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:111)
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:260)
at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:242)
Caused by: java.lang.IllegalStateException: Error processing condition on org.springframework.boot.resttestclient.autoconfigure.TestRestTemplateTestAutoConfiguration.testRestTemplate
at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:60)
at org.springframework.context.annotation.ConditionEvaluator.shouldSkip(ConditionEvaluator.java:100)
at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForBeanMethod(ConfigurationClassBeanDefinitionReader.java:196)
at org.springframework.context.annotation.ConfigurationClassBeanDefinitionReader.loadBeanDefinitionsForConfigurationClass(ConfigurationClassBeanDefinitionReader.java:148)
at org.springframework.context.annotation.ConfigurationClassPostProcessor.processConfigBeanDefinitions(ConfigurationClassPostProcessor.java:464)
at org.springframework.context.annotation.ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry(ConfigurationClassPostProcessor.java:316)
at org.springframework.context.support.PostProcessorRegistrationDelegate.invokeBeanDefinitionRegistryPostProcessors(PostProcessorRegistrationDelegate.java:349)
at org.springframework.context.support.AbstractApplicationContext.invokeBeanFactoryPostProcessors(AbstractApplicationContext.java:795)
at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:603)
at org.springframework.boot.web.server.servlet.context.ServletWebServerApplicationContext.refresh(ServletWebServerApplicationContext.java:143)
at org.springframework.boot.SpringApplication.refresh(SpringApplication.java:756)
at org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:445)
at org.springframework.boot.SpringApplication.run(SpringApplication.java:321)
Caused by: java.lang.IllegalStateException: @ConditionalOnMissingBean did not specify a bean using type, name or annotation and the attempt to deduce the bean's type failed
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.validate(OnBeanCondition.java:657)
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.<init>(OnBeanCondition.java:603)
at org.springframework.boot.autoconfigure.condition.OnBeanCondition.getMatchOutcome(OnBeanCondition.java:147)
at org.springframework.boot.autoconfigure.condition.SpringBootCondition.matches(SpringBootCondition.java:47)
Caused by: org.springframework.boot.autoconfigure.condition.OnBeanCondition$BeanTypeDeductionException: Failed to deduce bean type for org.springframework.boot.resttestclient.autoconfigure.TestRestTemplateTestAutoConfiguration.testRestTemplate
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.deducedBeanTypeForBeanMethod(OnBeanCondition.java:686)
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.deducedBeanType(OnBeanCondition.java:676)
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.<init>(OnBeanCondition.java:596)
Caused by: java.lang.IllegalStateException: Failed to introspect Class [org.springframework.boot.resttestclient.autoconfigure.TestRestTemplateTestAutoConfiguration] from ClassLoader [jdk.internal.loader.ClassLoaders$AppClassLoader@c387f44]
at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:483)
at org.springframework.util.ReflectionUtils.findMethod(ReflectionUtils.java:240)
at org.springframework.util.ReflectionUtils.findMethod(ReflectionUtils.java:221)
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.findBeanMethod(OnBeanCondition.java:715)
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.getMethodReturnType(OnBeanCondition.java:710)
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.getReturnType(OnBeanCondition.java:694)
at org.springframework.boot.autoconfigure.condition.OnBeanCondition$Spec.deducedBeanTypeForBeanMethod(OnBeanCondition.java:683)
Caused by: java.lang.NoClassDefFoundError: org/springframework/boot/restclient/RestTemplateBuilder
at java.base/java.lang.Class.getDeclaredMethods0(Native Method)
at java.base/java.lang.Class.privateGetDeclaredMethods(Class.java:3010)
at java.base/java.lang.Class.getDeclaredMethods(Class.java:2329)
at org.springframework.util.ReflectionUtils.getDeclaredMethods(ReflectionUtils.java:465)
Caused by: java.lang.ClassNotFoundException: org.springframework.boot.restclient.RestTemplateBuilder
at java.base/jdk.internal.loader.BuiltinClassLoader.loadClass(BuiltinClassLoader.java:580)
at java.base/java.lang.ClassLoader.loadClass(ClassLoader.java:490)
@@ -0,0 +1,23 @@
$ mvn test -Dtest=PersonEndToEndTest
# captured with @AutoConfigureRestTestClient removed from PersonEndToEndTest: @SpringBootTest alone
# never gives you an injectable RestTestClient, unlike TestRestTemplate's old free-auto-configuration
# behavior in Spring Boot 3.
[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)}
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:767)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:748)
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:146)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:493)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1444)
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireBeanProperties(AbstractAutowireCapableBeanFactory.java:402)
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:157)
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:111)
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:260)
at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:242)
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: 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)}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:2304)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1732)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1658)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:764)
@@ -0,0 +1,25 @@
$ mvn test -Dtest=TestRestTemplateMigrationTest
# captured with @AutoConfigureTestRestTemplate removed, but spring-boot-starter-restclient still
# present on the classpath. A completely different, much cleaner failure than
# 02-testresttemplate-missing-restclient-dependency.txt: this time the class loads fine, the
# condition evaluates fine, and Spring just never created the bean, because nothing asked it to.
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)}
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:767) ~[spring-beans-7.0.9.jar:7.0.9]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:748) ~[spring-beans-7.0.9.jar:7.0.9]
at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:146) ~[spring-beans-7.0.9.jar:7.0.9]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessProperties(AutowiredAnnotationBeanPostProcessor.java:493) ~[spring-beans-7.0.9.jar:7.0.9]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1444) ~[spring-beans-7.0.9.jar:7.0.9]
at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.autowireBeanProperties(AbstractAutowireCapableBeanFactory.java:402) ~[spring-beans-7.0.9.jar:7.0.9]
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.injectDependencies(DependencyInjectionTestExecutionListener.java:157) ~[spring-test-7.0.9.jar:7.0.9]
at org.springframework.test.context.support.DependencyInjectionTestExecutionListener.prepareTestInstance(DependencyInjectionTestExecutionListener.java:111) ~[spring-test-7.0.9.jar:7.0.9]
at org.springframework.test.context.TestContextManager.prepareTestInstance(TestContextManager.java:260) ~[spring-test-7.0.9.jar:7.0.9]
at org.springframework.test.context.junit.jupiter.SpringExtension.postProcessTestInstance(SpringExtension.java:242) ~[spring-test-7.0.9.jar:7.0.9]
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: 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)}
at org.springframework.beans.factory.support.DefaultListableBeanFactory.raiseNoMatchingBeanFound(DefaultListableBeanFactory.java:2304) ~[spring-beans-7.0.9.jar:7.0.9]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:1732) ~[spring-beans-7.0.9.jar:7.0.9]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:1658) ~[spring-beans-7.0.9.jar:7.0.9]
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.resolveFieldValue(AutowiredAnnotationBeanPostProcessor.java:764) ~[spring-beans-7.0.9.jar:7.0.9]
... 88 common frames omitted
[ERROR] Tests run: 1, Failures: 0, Errors: 1, Skipped: 0, Time elapsed: 5.107 s <<< FAILURE! -- in com.ankurm.resttestclient.TestRestTemplateMigrationTest
@@ -0,0 +1,8 @@
$ java -jar target/rest-test-client.jar --server.port=18080 &
$ 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
Date: Thu, 08 Oct 2026 09:12:37 GMT
@@ -0,0 +1,9 @@
$ mvn test -Dtest=PersonEndToEndTest
[INFO] Running com.ankurm.resttestclient.PersonEndToEndTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 6.971 s -- in com.ankurm.resttestclient.PersonEndToEndTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
# theResponseIsCompressedOnTheWireButTheClientHidesIt asserts Content-Encoding is ABSENT from the
# response RestTestClient.bindToServer() sees, against the same /api/persons endpoint
# 05-curl-sees-real-gzip.txt just proved really does send Content-Encoding: gzip. It passes: the
# default client hides it.
@@ -0,0 +1,12 @@
$ mvn test -Dtest=HttpClientChoiceComparisonTest
[INFO] Running com.ankurm.resttestclient.HttpClientChoiceComparisonTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 7.876 s -- in com.ankurm.resttestclient.HttpClientChoiceComparisonTest
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
# Same real server, same request, two different ClientHttpRequestFactory implementations:
# theJdkHttpClientDecodesGzipAndHidesTheHeader asserts Content-Encoding is absent through
# JdkClientHttpRequestFactory (java.net.http.HttpClient) -- the one Boot's
# ClientHttpRequestFactoryBuilder.detect() actually picks on this classpath.
# theOldHttpUrlConnectionFactoryShowsTheRealHeader asserts Content-Encoding equals "gzip" through
# SimpleClientHttpRequestFactory (java.net.HttpURLConnection) instead. Both pass: the two clients
# genuinely disagree about what they show you for the identical bytes on the wire.
@@ -0,0 +1,48 @@
$ unzip -oq spring-test-7.0.9.jar "org/springframework/test/web/servlet/client/RestTestClient*.class"
$ javap -p org/springframework/test/web/servlet/client/RestTestClient.class
public interface org.springframework.test.web.servlet.client.RestTestClient {
public static final java.lang.String RESTTESTCLIENT_REQUEST_ID;
public abstract org.springframework.test.web.servlet.client.RestTestClient$RequestHeadersUriSpec<?> get();
public abstract org.springframework.test.web.servlet.client.RestTestClient$RequestHeadersUriSpec<?> head();
public abstract org.springframework.test.web.servlet.client.RestTestClient$RequestBodyUriSpec post();
public abstract org.springframework.test.web.servlet.client.RestTestClient$RequestBodyUriSpec put();
public abstract org.springframework.test.web.servlet.client.RestTestClient$RequestBodyUriSpec patch();
public abstract org.springframework.test.web.servlet.client.RestTestClient$RequestHeadersUriSpec<?> delete();
public abstract org.springframework.test.web.servlet.client.RestTestClient$RequestHeadersUriSpec<?> options();
public abstract org.springframework.test.web.servlet.client.RestTestClient$RequestBodyUriSpec method(org.springframework.http.HttpMethod);
public abstract <B extends org.springframework.test.web.servlet.client.RestTestClient$Builder<B>> org.springframework.test.web.servlet.client.RestTestClient$Builder<B> mutate();
public static org.springframework.test.web.servlet.client.RestTestClient$StandaloneSetupBuilder bindToController(java.lang.Object...);
public static org.springframework.test.web.servlet.client.RestTestClient$RouterFunctionSetupBuilder bindToRouterFunction(org.springframework.web.servlet.function.RouterFunction<?>...);
public static org.springframework.test.web.servlet.client.RestTestClient$WebAppContextSetupBuilder bindToApplicationContext(org.springframework.web.context.WebApplicationContext);
public static org.springframework.test.web.servlet.client.RestTestClient$Builder<?> bindTo(org.springframework.test.web.servlet.MockMvc);
public static org.springframework.test.web.servlet.client.RestTestClient$Builder<?> bindToServer();
public static org.springframework.test.web.servlet.client.RestTestClient$Builder<?> bindToServer(org.springframework.http.client.ClientHttpRequestFactory);
}
$ 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);
}
# No multipart() method and no MultiValueMap<String, Object> body builder anywhere in the chain --
# body(Object) is the only way to set a request body. Confirms the multipart limitation directly
# against the real class files instead of trusting a blog post's claim.