Use Case
I run my spring boot app using a different hostname than localhost so I need to provide my own UriTemplateHandler
(which changes the hostname from localhost to something custom) instance to the TestRestTemplate
for integration testing.
Expected Solution
I thought the solution to my use case is to provide a RestTestTemplateBuilder
bean using a UriTemplateHandler
with the desired functionality as a test configuration:
@TestConfiguration
static class TestRestTemplateAuthenticationConfiguration {
@Bean
public RestTemplateBuilder restTemplateBuilder(Environment environment) {
LocalHostUriTemplateHandler ownTemplateHandler = new LocalHostUriTemplateHandler(environment, "https") {
@Override
public String getRootUri() {
return super.getRootUri().replaceAll("localhost", "custom-host.local");
}
};
return new RestTemplateBuilder().uriTemplateHandler(ownTemplateHandler);
}
}
Problematic Code
To my surprise, this didn't work since the provided uriTemplateHandler
is replaced by a newly created instance in the TestRestTemplateFactory
as you can see in this code snippet:
https://github.com/spring-projects/spring-boot/blob/a664eadb9a0fb40a97fb579476883f4bfe832ed5/spring-boot-project/spring-boot-test/src/main/java/org/springframework/boot/test/web/client/TestRestTemplateContextCustomizer.java#L137-L146
This behavior is quite unexpected and looks like a bug to me. Can someone confirm that this is in fact a bug? If this is a confirmed bug, I am willing to submit a pull request.
Comment From: philwebb
@fanngrim This looks like it might be an ordering issue where it's not possible to override our LocalHostUriTemplateHandler
. Are you able to share a small complete example that we can download and run that shows how you configure your test?
Comment From: spring-projects-issues
If you would like us to look at this issue, please provide the requested information. If the information is not provided within the next 7 days this issue will be closed.
Comment From: fanngrim
Hi @philwebb thanks for looking into this and sorry that it has taken me so long to reply.
Here is a example project: override-uritemplatehandler.zip If the configuration would work properly, the test is supposed to fail.
Though I have to say I don't understand why this would be an ordering issue. In the code snipped that I mentioned in the original post, you can see that there is a fresh instance of the LocalHostUriTemplateHandler created and just set to the TestRestTemplate. The custom builder (which I am providing) has already finished it's work at that moment since it gets used as a argument in the constructor of TestRestTemplate. Am I missing something here?
Comment From: wilkinsona
Thanks for the sample, @fanngrim.
As you've noted above, TestRestTemplateFactory
is overriding the customization you've made on your RestTemplateBuilder
bean by setting the handler once it's created the TestRestTemplate
. The problem that we have is that it's hard to know when it is and is not appropriate to override the handler. For example, there may be a RestTemplateBuilder
in the main application code that customises the handler and this should be overridden by TestRestTemplateFactory
. Equally, there may be a situation like yours where it's not appropriate to override the configuration.
The auto-configured TestRestTemplate
is intended for testing the local, embedded server. In your situation where you want to call another service in an integration test, I think you'd be better providing your own TestRestTemplate
bean. This will cause the auto-configured one to back off. You can achieve that with the following configuration:
@TestConfiguration
static class TestRestTemplateAuthenticationConfiguration {
@Bean
public TestRestTemplate testRestTemplate(Environment environment) {
LocalHostUriTemplateHandler ownTemplateHandler = new LocalHostUriTemplateHandler(environment, "http") {
@Override
public String getRootUri() {
return super.getRootUri().replaceAll("localhost", "my-host.local");
}
};
return new TestRestTemplate(new RestTemplateBuilder().uriTemplateHandler(ownTemplateHandler));
}
}
With this in place the sample test fails as expected:
org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://my-host.local:59544/rest/hello": my-host.local; nested exception is java.net.UnknownHostException: my-host.local
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:751)
at org.springframework.web.client.RestTemplate.execute(RestTemplate.java:677)
at org.springframework.web.client.RestTemplate.getForEntity(RestTemplate.java:345)
at org.springframework.boot.test.web.client.TestRestTemplate.getForEntity(TestRestTemplate.java:233)
at com.example.overrideuritemplatehandler.HelloWorldTests.testHelloWorld(HelloWorldTests.java:35)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:498)
at org.junit.platform.commons.util.ReflectionUtils.invokeMethod(ReflectionUtils.java:675)
at org.junit.jupiter.engine.execution.MethodInvocation.proceed(MethodInvocation.java:60)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$ValidatingInvocation.proceed(InvocationInterceptorChain.java:125)
at org.junit.jupiter.engine.extension.TimeoutExtension.intercept(TimeoutExtension.java:132)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestableMethod(TimeoutExtension.java:124)
at org.junit.jupiter.engine.extension.TimeoutExtension.interceptTestMethod(TimeoutExtension.java:74)
at org.junit.jupiter.engine.execution.ExecutableInvoker$ReflectiveInterceptorCall.lambda$ofVoidMethod$0(ExecutableInvoker.java:115)
at org.junit.jupiter.engine.execution.ExecutableInvoker.lambda$invoke$0(ExecutableInvoker.java:105)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain$InterceptedInvocation.proceed(InvocationInterceptorChain.java:104)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.proceed(InvocationInterceptorChain.java:62)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.chainAndInvoke(InvocationInterceptorChain.java:43)
at org.junit.jupiter.engine.execution.InvocationInterceptorChain.invoke(InvocationInterceptorChain.java:35)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:104)
at org.junit.jupiter.engine.execution.ExecutableInvoker.invoke(ExecutableInvoker.java:98)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.lambda$invokeTestMethod$6(TestMethodTestDescriptor.java:202)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.invokeTestMethod(TestMethodTestDescriptor.java:198)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:135)
at org.junit.jupiter.engine.descriptor.TestMethodTestDescriptor.execute(TestMethodTestDescriptor.java:69)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:135)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at java.util.ArrayList.forEach(ArrayList.java:1257)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at java.util.ArrayList.forEach(ArrayList.java:1257)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.invokeAll(SameThreadHierarchicalTestExecutorService.java:38)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$5(NodeTestTask.java:139)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$7(NodeTestTask.java:125)
at org.junit.platform.engine.support.hierarchical.Node.around(Node.java:135)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.lambda$executeRecursively$8(NodeTestTask.java:123)
at org.junit.platform.engine.support.hierarchical.ThrowableCollector.execute(ThrowableCollector.java:73)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.executeRecursively(NodeTestTask.java:122)
at org.junit.platform.engine.support.hierarchical.NodeTestTask.execute(NodeTestTask.java:80)
at org.junit.platform.engine.support.hierarchical.SameThreadHierarchicalTestExecutorService.submit(SameThreadHierarchicalTestExecutorService.java:32)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestExecutor.execute(HierarchicalTestExecutor.java:57)
at org.junit.platform.engine.support.hierarchical.HierarchicalTestEngine.execute(HierarchicalTestEngine.java:51)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:229)
at org.junit.platform.launcher.core.DefaultLauncher.lambda$execute$6(DefaultLauncher.java:197)
at org.junit.platform.launcher.core.DefaultLauncher.withInterceptedStreams(DefaultLauncher.java:211)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:191)
at org.junit.platform.launcher.core.DefaultLauncher.execute(DefaultLauncher.java:137)
at org.eclipse.jdt.internal.junit5.runner.JUnit5TestReference.run(JUnit5TestReference.java:89)
at org.eclipse.jdt.internal.junit.runner.TestExecution.run(TestExecution.java:41)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:542)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.runTests(RemoteTestRunner.java:770)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.run(RemoteTestRunner.java:464)
at org.eclipse.jdt.internal.junit.runner.RemoteTestRunner.main(RemoteTestRunner.java:210)
Caused by: java.net.UnknownHostException: my-host.local
at java.net.AbstractPlainSocketImpl.connect(AbstractPlainSocketImpl.java:184)
at java.net.SocksSocketImpl.connect(SocksSocketImpl.java:392)
at java.net.Socket.connect(Socket.java:589)
at java.net.Socket.connect(Socket.java:538)
at sun.net.NetworkClient.doConnect(NetworkClient.java:180)
at sun.net.www.http.HttpClient.openServer(HttpClient.java:463)
at sun.net.www.http.HttpClient.openServer(HttpClient.java:558)
at sun.net.www.http.HttpClient.<init>(HttpClient.java:242)
at sun.net.www.http.HttpClient.New(HttpClient.java:339)
at sun.net.www.http.HttpClient.New(HttpClient.java:357)
at sun.net.www.protocol.http.HttpURLConnection.getNewHttpClient(HttpURLConnection.java:1220)
at sun.net.www.protocol.http.HttpURLConnection.plainConnect0(HttpURLConnection.java:1156)
at sun.net.www.protocol.http.HttpURLConnection.plainConnect(HttpURLConnection.java:1050)
at sun.net.www.protocol.http.HttpURLConnection.connect(HttpURLConnection.java:984)
at org.springframework.http.client.SimpleBufferingClientHttpRequest.executeInternal(SimpleBufferingClientHttpRequest.java:76)
at org.springframework.http.client.AbstractBufferingClientHttpRequest.executeInternal(AbstractBufferingClientHttpRequest.java:48)
at org.springframework.http.client.AbstractClientHttpRequest.execute(AbstractClientHttpRequest.java:53)
at org.springframework.web.client.RestTemplate.doExecute(RestTemplate.java:742)
... 69 more
I don't think there's a change that we can make here that will work for everyone, and hopefully the suggestion above meets your needs so I'm going to close this issue. If there's something that I've overlooked that means that the above does not help, please let us know and we can take another look.
Comment From: fanngrim
Thanks for the detailed explanation @wilkinsona, I definitely see your point now!
I already thought about just providing my own TestRestTemplate bean directly but I didn't think this would be the proper way to do since I thought it would be more elegant to do it via the builder.
Though I get the problem now and I agree that just providing a TestRestTemplate bean will be the best solution for me.