Skip to content

Commit 2eafb3d

Browse files
committed
Add @restclienttest support
Add @restclienttest annotation that can be used when testing REST clients. Provides auto-configuration for a MockRestServiceServer which can be used when the bean under test builds a single RestTemplate via the auto-configured RestTemplateBuilder. Closes spring-projectsgh-6030
1 parent b382310 commit 2eafb3d

22 files changed

+1751
-1
lines changed
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/*
2+
* Copyright 2012-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.test.autoconfigure.web.client;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
26+
import org.springframework.boot.test.autoconfigure.properties.PropertyMapping;
27+
import org.springframework.boot.test.web.client.MockServerRestTemplateCustomizer;
28+
import org.springframework.boot.web.client.RestTemplateBuilder;
29+
import org.springframework.test.web.client.MockRestServiceServer;
30+
31+
/**
32+
* Annotation that can be applied to a test class to enable and configure
33+
* auto-configuration of a single {@link MockRestServiceServer}. Only useful when a single
34+
* call is made to {@link RestTemplateBuilder}. If multiple
35+
* {@link org.springframework.web.client.RestTemplate RestTemplates} are in use, inject
36+
* {@link MockServerRestTemplateCustomizer} and use
37+
* {@link MockServerRestTemplateCustomizer#getServer(org.springframework.web.client.RestTemplate)
38+
* getServer(RestTemplate)} or bind a {@link MockRestServiceServer} directly.
39+
*
40+
* @author Phillip Webb
41+
* @since 1.4.0
42+
* @see MockServerRestTemplateCustomizer
43+
*/
44+
@Target(ElementType.TYPE)
45+
@Retention(RetentionPolicy.RUNTIME)
46+
@Documented
47+
@ImportAutoConfiguration
48+
@PropertyMapping("spring.test.webclient.mockrestserviceserver")
49+
public @interface AutoConfigureMockRestServiceServer {
50+
51+
/**
52+
* If {@link MockServerRestTemplateCustomizer} should be enabled and
53+
* {@link MockRestServiceServer} beans should be registered. Defaults to {@code true}
54+
* @return if mock support is enabled
55+
*/
56+
boolean enabled() default true;
57+
58+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
/*
2+
* Copyright 2012-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.test.autoconfigure.web.client;
18+
19+
import java.lang.annotation.Documented;
20+
import java.lang.annotation.ElementType;
21+
import java.lang.annotation.Retention;
22+
import java.lang.annotation.RetentionPolicy;
23+
import java.lang.annotation.Target;
24+
25+
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
26+
import org.springframework.boot.test.autoconfigure.properties.PropertyMapping;
27+
import org.springframework.boot.web.client.RestTemplateBuilder;
28+
import org.springframework.web.client.RestTemplate;
29+
30+
/**
31+
* Annotation that can be applied to a test class to enable and configure
32+
* auto-configuration of web clients.
33+
*
34+
* @author Stephane Nicoll
35+
* @author Phillip Webb
36+
* @since 1.4.0
37+
*/
38+
@Target(ElementType.TYPE)
39+
@Retention(RetentionPolicy.RUNTIME)
40+
@Documented
41+
@ImportAutoConfiguration
42+
@PropertyMapping("spring.test.webclient")
43+
public @interface AutoConfigureWebClient {
44+
45+
/**
46+
* If a {@link RestTemplate} bean should be registered. Defaults to {@code false} with
47+
* the assumption that the {@link RestTemplateBuilder} will be used.
48+
* @return if a {@link RestTemplate} bean should be added.
49+
*/
50+
boolean registerRestTemplate() default false;
51+
52+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
/*
2+
* Copyright 2012-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.test.autoconfigure.web.client;
18+
19+
import java.io.IOException;
20+
import java.lang.reflect.Constructor;
21+
import java.util.Map;
22+
23+
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
24+
import org.springframework.boot.test.web.client.MockServerRestTemplateCustomizer;
25+
import org.springframework.context.annotation.Bean;
26+
import org.springframework.context.annotation.Configuration;
27+
import org.springframework.http.client.ClientHttpRequest;
28+
import org.springframework.http.client.ClientHttpResponse;
29+
import org.springframework.test.web.client.ExpectedCount;
30+
import org.springframework.test.web.client.MockRestServiceServer;
31+
import org.springframework.test.web.client.RequestExpectationManager;
32+
import org.springframework.test.web.client.RequestMatcher;
33+
import org.springframework.test.web.client.ResponseActions;
34+
import org.springframework.util.Assert;
35+
import org.springframework.web.client.RestTemplate;
36+
37+
/**
38+
* Auto-configuration for {@link MockRestServiceServer} support.
39+
*
40+
* @author Phillip Webb
41+
* @see AutoConfigureMockRestServiceServer
42+
*/
43+
@Configuration
44+
@ConditionalOnProperty(prefix = "spring.test.webclient.mockrestserviceserver", name = "enabled")
45+
class MockRestServiceServerAutoConfiguration {
46+
47+
@Bean
48+
public MockServerRestTemplateCustomizer mockServerRestTemplateCustomizer() {
49+
return new MockServerRestTemplateCustomizer();
50+
}
51+
52+
@Bean
53+
public MockRestServiceServer mockRestServiceServer(
54+
MockServerRestTemplateCustomizer customizer) {
55+
try {
56+
return createDeferredMockRestServiceServer(customizer);
57+
}
58+
catch (Exception ex) {
59+
throw new IllegalStateException(ex);
60+
}
61+
}
62+
63+
private MockRestServiceServer createDeferredMockRestServiceServer(
64+
MockServerRestTemplateCustomizer customizer) throws Exception {
65+
Constructor<MockRestServiceServer> constructor = MockRestServiceServer.class
66+
.getDeclaredConstructor(RequestExpectationManager.class);
67+
constructor.setAccessible(true);
68+
return constructor.newInstance(new DeferredRequestExpectationManager(customizer));
69+
}
70+
71+
/**
72+
* {@link RequestExpectationManager} with the injected {@link MockRestServiceServer}
73+
* so that the bean can be created before the
74+
* {@link MockServerRestTemplateCustomizer#customize(RestTemplate)
75+
* MockServerRestTemplateCustomizer} has been called.
76+
*/
77+
private static class DeferredRequestExpectationManager
78+
implements RequestExpectationManager {
79+
80+
private MockServerRestTemplateCustomizer customizer;
81+
82+
DeferredRequestExpectationManager(MockServerRestTemplateCustomizer customizer) {
83+
this.customizer = customizer;
84+
}
85+
86+
@Override
87+
public ResponseActions expectRequest(ExpectedCount count,
88+
RequestMatcher requestMatcher) {
89+
return getDelegate().expectRequest(count, requestMatcher);
90+
}
91+
92+
@Override
93+
public ClientHttpResponse validateRequest(ClientHttpRequest request)
94+
throws IOException {
95+
return getDelegate().validateRequest(request);
96+
}
97+
98+
@Override
99+
public void verify() {
100+
getDelegate().verify();
101+
}
102+
103+
@Override
104+
public void reset() {
105+
Map<RestTemplate, RequestExpectationManager> expectationManagers = this.customizer
106+
.getExpectationManagers();
107+
if (expectationManagers.size() == 1) {
108+
getDelegate().reset();
109+
}
110+
}
111+
112+
private RequestExpectationManager getDelegate() {
113+
Map<RestTemplate, RequestExpectationManager> expectationManagers = this.customizer
114+
.getExpectationManagers();
115+
Assert.state(expectationManagers.size() > 0,
116+
"Unable to use auto-configured MockRestServiceServer since "
117+
+ "MockServerRestTemplateCustomizer has not been bound to "
118+
+ "a RestTemplate");
119+
Assert.state(expectationManagers.size() == 1,
120+
"Unable to use auto-configured MockRestServiceServer since "
121+
+ "MockServerRestTemplateCustomizer has been bound to "
122+
+ "more than one RestTemplate");
123+
return expectationManagers.values().iterator().next();
124+
}
125+
126+
}
127+
128+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
/*
2+
* Copyright 2012-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.test.autoconfigure.web.client;
18+
19+
import org.springframework.context.ApplicationContext;
20+
import org.springframework.test.context.TestContext;
21+
import org.springframework.test.context.TestExecutionListener;
22+
import org.springframework.test.context.support.AbstractTestExecutionListener;
23+
import org.springframework.test.web.client.MockRestServiceServer;
24+
25+
/**
26+
* {@link TestExecutionListener} to reset {@link MockRestServiceServer} beans.
27+
*
28+
* @author Phillip Webb
29+
*/
30+
class MockRestServiceServerResetTestExecutionListener
31+
extends AbstractTestExecutionListener {
32+
33+
@Override
34+
public void afterTestMethod(TestContext testContext) throws Exception {
35+
ApplicationContext applicationContext = testContext.getApplicationContext();
36+
String[] names = applicationContext
37+
.getBeanNamesForType(MockRestServiceServer.class, false, false);
38+
for (String name : names) {
39+
applicationContext.getBean(name, MockRestServiceServer.class).reset();
40+
}
41+
}
42+
43+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,97 @@
1+
/*
2+
* Copyright 2012-2016 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* http://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.boot.test.autoconfigure.web.client;
18+
19+
import java.io.IOException;
20+
import java.util.Collections;
21+
import java.util.LinkedHashSet;
22+
import java.util.Set;
23+
24+
import com.fasterxml.jackson.databind.Module;
25+
26+
import org.springframework.boot.context.TypeExcludeFilter;
27+
import org.springframework.boot.jackson.JsonComponent;
28+
import org.springframework.boot.test.autoconfigure.filter.AnnotationCustomizableTypeExcludeFilter;
29+
import org.springframework.context.annotation.ComponentScan.Filter;
30+
import org.springframework.core.annotation.AnnotatedElementUtils;
31+
import org.springframework.core.type.classreading.MetadataReader;
32+
import org.springframework.core.type.classreading.MetadataReaderFactory;
33+
34+
/**
35+
* {@link TypeExcludeFilter} for {@link RestClientTest @RestClientTest}.
36+
*
37+
* @author Stephane Nicoll
38+
*/
39+
class RestClientExcludeFilter extends AnnotationCustomizableTypeExcludeFilter {
40+
41+
private static final Set<Class<?>> DEFAULT_INCLUDES;
42+
43+
static {
44+
Set<Class<?>> includes = new LinkedHashSet<Class<?>>();
45+
includes.add(Module.class);
46+
includes.add(JsonComponent.class);
47+
DEFAULT_INCLUDES = Collections.unmodifiableSet(includes);
48+
}
49+
50+
private final RestClientTest annotation;
51+
52+
RestClientExcludeFilter(Class<?> testClass) {
53+
this.annotation = AnnotatedElementUtils.getMergedAnnotation(testClass,
54+
RestClientTest.class);
55+
}
56+
57+
@Override
58+
protected boolean defaultInclude(MetadataReader metadataReader,
59+
MetadataReaderFactory metadataReaderFactory) throws IOException {
60+
if (super.defaultInclude(metadataReader, metadataReaderFactory)) {
61+
return true;
62+
}
63+
for (Class<?> controller : this.annotation.components()) {
64+
if (isTypeOrAnnotated(metadataReader, metadataReaderFactory, controller)) {
65+
return true;
66+
}
67+
}
68+
return false;
69+
}
70+
71+
@Override
72+
protected boolean hasAnnotation() {
73+
return this.annotation != null;
74+
}
75+
76+
@Override
77+
protected Filter[] getFilters(FilterType type) {
78+
switch (type) {
79+
case INCLUDE:
80+
return this.annotation.includeFilters();
81+
case EXCLUDE:
82+
return this.annotation.excludeFilters();
83+
}
84+
throw new IllegalStateException("Unsupported type " + type);
85+
}
86+
87+
@Override
88+
protected boolean isUseDefaultFilters() {
89+
return this.annotation.useDefaultFilters();
90+
}
91+
92+
@Override
93+
protected Set<Class<?>> getDefaultIncludes() {
94+
return DEFAULT_INCLUDES;
95+
}
96+
97+
}

0 commit comments

Comments
 (0)