Add GraphQlTest annotation

Prior to this commit, we could only test Spring GraphQL applications
with a complete application - all application and infrastructure
components were involved. While using `@SpringBootTest` is often useful
for complete integration tests (with or without a live running server),
we often want to write lean integration tests and test slices of our
application.

Just like `@WebMvcTest` or `@WebFluxTest`, this commit introduces the
support for `@GraphQlTest`. This annotation helps us to test a
particular slice of our application: a hand-picked selection of
`@Controller`, plus `RuntimeWiringConfigurer` and `WebInterceptor`
beans.

Other `@Component` must be imported or mocked for those tests.

This commit also refactors the existing auto-configuration to enable
this use case. The `WebGraphQlHandlerAutoConfiguration` now holds the
required components for `@GraphQlTest`, while other web-related
auto-configurations bring the web framework and transport
infrastructures.

Closes gh-75
This commit is contained in:
Brian Clozel
2021-10-22 21:50:05 +02:00
parent 69e34baadf
commit e053e14158
27 changed files with 901 additions and 134 deletions

View File

@@ -17,14 +17,11 @@
package org.springframework.graphql.boot;
import java.util.Collections;
import java.util.stream.Collectors;
import graphql.GraphQL;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -37,12 +34,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.web.WebInterceptor;
import org.springframework.graphql.web.webflux.GraphQlHttpHandler;
import org.springframework.graphql.web.webflux.GraphQlWebSocketHandler;
import org.springframework.graphql.web.webflux.GraphiQlHandler;
@@ -75,27 +68,12 @@ import static org.springframework.web.reactive.function.server.RequestPredicates
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.REACTIVE)
@ConditionalOnClass({GraphQL.class, GraphQlHttpHandler.class})
@ConditionalOnBean(GraphQlSource.class)
@AutoConfigureAfter(GraphQlServiceAutoConfiguration.class)
@AutoConfigureAfter(WebGraphQlHandlerAutoConfiguration.class)
@EnableConfigurationProperties(GraphQlCorsProperties.class)
public class GraphQlWebFluxAutoConfiguration {
private static final Log logger = LogFactory.getLog(GraphQlWebFluxAutoConfiguration.class);
@Bean
public AnnotatedControllerConfigurer annotatedControllerConfigurer(@Qualifier("webFluxConversionService") FormattingConversionService conversionService) {
AnnotatedControllerConfigurer annotatedControllerConfigurer = new AnnotatedControllerConfigurer();
annotatedControllerConfigurer.setConversionService(conversionService);
return annotatedControllerConfigurer;
}
@Bean
@ConditionalOnBean(GraphQlService.class)
@ConditionalOnMissingBean
public WebGraphQlHandler webGraphQlHandler(GraphQlService service, ObjectProvider<WebInterceptor> interceptors) {
return WebGraphQlHandler.builder(service)
.interceptors(interceptors.orderedStream().collect(Collectors.toList())).build();
}
@Bean
@ConditionalOnMissingBean
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {

View File

@@ -18,7 +18,6 @@ package org.springframework.graphql.boot;
import java.util.Collections;
import java.util.Map;
import java.util.stream.Collectors;
import javax.websocket.server.ServerContainer;
@@ -26,8 +25,6 @@ import graphql.GraphQL;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -41,13 +38,8 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.Resource;
import org.springframework.core.io.ResourceLoader;
import org.springframework.format.support.FormattingConversionService;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.ThreadLocalAccessor;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.web.WebInterceptor;
import org.springframework.graphql.web.webmvc.GraphQlHttpHandler;
import org.springframework.graphql.web.webmvc.GraphQlWebSocketHandler;
import org.springframework.graphql.web.webmvc.GraphiQlHandler;
@@ -82,29 +74,12 @@ import static org.springframework.web.servlet.function.RequestPredicates.content
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
@ConditionalOnClass({GraphQL.class, GraphQlHttpHandler.class})
@ConditionalOnBean(GraphQlSource.class)
@AutoConfigureAfter(GraphQlServiceAutoConfiguration.class)
@AutoConfigureAfter(WebGraphQlHandlerAutoConfiguration.class)
@EnableConfigurationProperties(GraphQlCorsProperties.class)
public class GraphQlWebMvcAutoConfiguration {
private static final Log logger = LogFactory.getLog(GraphQlWebMvcAutoConfiguration.class);
@Bean
public AnnotatedControllerConfigurer annotatedControllerConfigurer(@Qualifier("mvcConversionService") FormattingConversionService conversionService) {
AnnotatedControllerConfigurer annotatedControllerConfigurer = new AnnotatedControllerConfigurer();
annotatedControllerConfigurer.setConversionService(conversionService);
return annotatedControllerConfigurer;
}
@Bean
@ConditionalOnBean(GraphQlService.class)
@ConditionalOnMissingBean
public WebGraphQlHandler webGraphQlHandler(GraphQlService service, ObjectProvider<WebInterceptor> interceptorsProvider,
ObjectProvider<ThreadLocalAccessor> accessorsProvider) {
return WebGraphQlHandler.builder(service)
.interceptors(interceptorsProvider.orderedStream().collect(Collectors.toList()))
.threadLocalAccessors(accessorsProvider.orderedStream().collect(Collectors.toList())).build();
}
@Bean
@ConditionalOnMissingBean
public GraphQlHttpHandler graphQlHttpHandler(WebGraphQlHandler webGraphQlHandler) {

View File

@@ -16,23 +16,31 @@
package org.springframework.graphql.boot;
import java.util.stream.Collectors;
import graphql.GraphQL;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.DefaultBatchLoaderRegistry;
import org.springframework.graphql.execution.ExecutionGraphQlService;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.execution.ThreadLocalAccessor;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.web.WebInterceptor;
/**
* {@link EnableAutoConfiguration Auto-configuration} for creating a
* {@link GraphQlService}.
* {@link WebGraphQlHandler}.
*
* @author Brian Clozel
* @since 1.0.0
@@ -41,11 +49,12 @@ import org.springframework.graphql.execution.GraphQlSource;
@ConditionalOnClass({GraphQL.class, GraphQlService.class})
@ConditionalOnMissingBean(GraphQlService.class)
@AutoConfigureAfter(GraphQlAutoConfiguration.class)
public class GraphQlServiceAutoConfiguration {
public class WebGraphQlHandlerAutoConfiguration {
private final BatchLoaderRegistry batchLoaderRegistry = new DefaultBatchLoaderRegistry();
@Bean
@ConditionalOnMissingBean
public BatchLoaderRegistry batchLoaderRegistry() {
return this.batchLoaderRegistry;
}
@@ -58,4 +67,21 @@ public class GraphQlServiceAutoConfiguration {
return service;
}
@Bean
@ConditionalOnMissingBean
public AnnotatedControllerConfigurer annotatedControllerConfigurer() {
AnnotatedControllerConfigurer annotatedControllerConfigurer = new AnnotatedControllerConfigurer();
annotatedControllerConfigurer.setConversionService(new DefaultFormattingConversionService());
return annotatedControllerConfigurer;
}
@Bean
@ConditionalOnMissingBean
public WebGraphQlHandler webGraphQlHandler(GraphQlService service, ObjectProvider<WebInterceptor> interceptorsProvider,
ObjectProvider<ThreadLocalAccessor> accessorsProvider) {
return WebGraphQlHandler.builder(service)
.interceptors(interceptorsProvider.orderedStream().collect(Collectors.toList()))
.threadLocalAccessors(accessorsProvider.orderedStream().collect(Collectors.toList())).build();
}
}

View File

@@ -0,0 +1,44 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot.test;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
/**
* {@link ImportAutoConfiguration Auto-configuration imports} for typical Spring GraphQL
* tests. Most tests should consider using {@link GraphQlTest @GraphQlTest} rather than
* using this annotation directly.
*
* @author Brian Clozel
* @since 1.0.0
* @see GraphQlTest
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ImportAutoConfiguration
public @interface AutoConfigureGraphQl {
}

View File

@@ -0,0 +1,143 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot.test;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.autoconfigure.OverrideAutoConfiguration;
import org.springframework.boot.test.autoconfigure.core.AutoConfigureCache;
import org.springframework.boot.test.autoconfigure.filter.TypeExcludeFilters;
import org.springframework.boot.test.autoconfigure.json.AutoConfigureJson;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.core.annotation.AliasFor;
import org.springframework.core.env.Environment;
import org.springframework.graphql.boot.test.tester.AutoConfigureWebGraphQlTester;
import org.springframework.test.context.BootstrapWith;
import org.springframework.test.context.junit.jupiter.SpringExtension;
/**
* Annotation that can be used for a Spring GraphQL test that focuses
* <strong>only</strong> on Spring GraphQL components, without involving web frameworks.
* <p>
* Using this annotation will disable full auto-configuration and instead apply only
* configuration relevant to GraphQL tests (i.e. {@code @Controller},
* {@code @JsonComponent}, {@code Converter}/{@code GenericConverter},
* {@code WebInterceptor} and {@code RuntimeWiringConfigurer} beans but not
* {@code @Component}, {@code @Service} or {@code @Repository} beans).
* <p>
* By default, tests annotated with {@code @GraphQlTest} will also auto-configure a
* {@link org.springframework.graphql.test.tester.WebGraphQlTester}. For more fine-grained control of GraphQlTester the
* {@link AutoConfigureWebGraphQlTester @AutoConfigureGraphQlTester} annotation can be used.
* <p>
* Typically {@code @GraphQlTest} is used in combination with
* {@link org.springframework.boot.test.mock.mockito.MockBean @MockBean} or
* {@link org.springframework.context.annotation.Import @Import} to create any collaborators required by your
* {@code @Controller} beans.
* <p>
* If you are looking to load your full application configuration and use {@code WebGraphQlTester},
* you should consider {@link org.springframework.boot.test.context.SpringBootTest @SpringBootTest} combined with
* {@link AutoConfigureWebGraphQlTester @AutoConfigureWebGraphQlTester} rather than this
* annotation.
*
* @author Brian Clozel
* @since 1.0.0
* @see AutoConfigureWebGraphQlTester
*/
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@BootstrapWith(GraphQlTestContextBootstrapper.class)
@ExtendWith(SpringExtension.class)
@OverrideAutoConfiguration(enabled = false)
@TypeExcludeFilters(GraphQlTypeExcludeFilter.class)
@AutoConfigureCache
@AutoConfigureJson
@AutoConfigureGraphQl
@AutoConfigureWebGraphQlTester
@ImportAutoConfiguration
public @interface GraphQlTest {
/**
* Properties in form {@literal key=value} that should be added to the Spring
* {@link Environment} before the test runs.
* @return the properties to add
*/
String[] properties() default {};
/**
* Specifies the controllers to test. This is an alias of {@link #controllers()} which
* can be used for brevity if no other attributes are defined. See
* {@link #controllers()} for details.
* @see #controllers()
* @return the controllers to test
*/
@AliasFor("controllers")
Class<?>[] value() default {};
/**
* Specifies the controllers to test. May be left blank if all {@code @Controller}
* beans should be added to the application context.
* @see #value()
* @return the controllers to test
*/
@AliasFor("value")
Class<?>[] controllers() default {};
/**
* Determines if default filtering should be used with
* {@link SpringBootApplication @SpringBootApplication}. By default, only
* {@code @Controller} (when no explicit {@link #controllers() controllers} are
* defined), {@code @ControllerAdvice}, {@code WebInterceptor}
* and {@code RuntimeWiringConfigurer} beans are
* included.
* @see #includeFilters()
* @see #excludeFilters()
* @return if default filters should be used
*/
boolean useDefaultFilters() default true;
/**
* A set of include filters which can be used to add otherwise filtered beans to the
* application context.
* @return include filters to apply
*/
ComponentScan.Filter[] includeFilters() default {};
/**
* A set of exclude filters which can be used to filter beans that would otherwise be
* added to the application context.
* @return exclude filters to apply
*/
ComponentScan.Filter[] excludeFilters() default {};
/**
* Auto-configuration exclusions that should be applied for this test.
* @return auto-configuration exclusions to apply
*/
@AliasFor(annotation = ImportAutoConfiguration.class, attribute = "exclude")
Class<?>[] excludeAutoConfiguration() default {};
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot.test;
import org.springframework.boot.test.context.SpringBootTestContextBootstrapper;
import org.springframework.core.annotation.MergedAnnotations;
import org.springframework.test.context.TestContextBootstrapper;
/**
* {@link TestContextBootstrapper} for {@link GraphQlTest @GraphQlTest}
*
* @author Brian Clozel
*/
class GraphQlTestContextBootstrapper extends SpringBootTestContextBootstrapper {
@Override
protected String[] getProperties(Class<?> testClass) {
return MergedAnnotations.from(testClass, MergedAnnotations.SearchStrategy.INHERITED_ANNOTATIONS).get(GraphQlTest.class)
.getValue("properties", String[].class).orElse(null);
}
}

View File

@@ -0,0 +1,94 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot.test;
import java.util.Arrays;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.boot.jackson.JsonComponent;
import org.springframework.boot.test.autoconfigure.filter.StandardAnnotationCustomizableTypeExcludeFilter;
import org.springframework.core.convert.converter.Converter;
import org.springframework.core.convert.converter.GenericConverter;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.web.WebInterceptor;
import org.springframework.stereotype.Controller;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
/**
* {@link TypeExcludeFilter} for {@link GraphQlTest @GraphQlTest}.
*
* @author Brian Clozel
* @since 1.0.0
*/
public class GraphQlTypeExcludeFilter extends StandardAnnotationCustomizableTypeExcludeFilter<GraphQlTest> {
private static final Class<?>[] NO_CONTROLLERS = {};
private static final String[] OPTIONAL_INCLUDES = { "com.fasterxml.jackson.databind.Module"};
private static final Set<Class<?>> DEFAULT_INCLUDES;
static {
Set<Class<?>> includes = new LinkedHashSet<>();
includes.add(JsonComponent.class);
includes.add(RuntimeWiringConfigurer.class);
includes.add(Converter.class);
includes.add(GenericConverter.class);
includes.add(WebInterceptor.class);
for (String optionalInclude : OPTIONAL_INCLUDES) {
try {
includes.add(ClassUtils.forName(optionalInclude, null));
}
catch (Exception ex) {
// Ignore
}
}
DEFAULT_INCLUDES = Collections.unmodifiableSet(includes);
}
private static final Set<Class<?>> DEFAULT_INCLUDES_AND_CONTROLLER;
static {
Set<Class<?>> includes = new LinkedHashSet<>(DEFAULT_INCLUDES);
includes.add(Controller.class);
DEFAULT_INCLUDES_AND_CONTROLLER = Collections.unmodifiableSet(includes);
}
private final Class<?>[] controllers;
GraphQlTypeExcludeFilter(Class<?> testClass) {
super(testClass);
this.controllers = getAnnotation().getValue("controllers", Class[].class).orElse(NO_CONTROLLERS);
}
@Override
protected Set<Class<?>> getDefaultIncludes() {
if (ObjectUtils.isEmpty(this.controllers)) {
return DEFAULT_INCLUDES_AND_CONTROLLER;
}
return DEFAULT_INCLUDES;
}
@Override
protected Set<Class<?>> getComponentIncludes() {
return new LinkedHashSet<>(Arrays.asList(this.controllers));
}
}

View File

@@ -24,20 +24,21 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.autoconfigure.ImportAutoConfiguration;
import org.springframework.graphql.test.tester.GraphQlTester;
import org.springframework.graphql.test.tester.WebGraphQlTester;
/**
* Annotation that can be applied to a test class to enable a {@link GraphQlTester}.
* Annotation that can be applied to a test class to enable a {@link WebGraphQlTester}.
*
* @author Brian Clozel
* @see GraphQlTesterAutoConfiguration
* @since 1.0.0
* @see WebTestClientMockMvcAutoConfiguration
* @see WebGraphQlTesterAutoConfiguration
*/
@Target({ ElementType.TYPE, ElementType.METHOD })
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@ImportAutoConfiguration
public @interface AutoConfigureGraphQlTester {
public @interface AutoConfigureWebGraphQlTester {
}

View File

@@ -16,37 +16,35 @@
package org.springframework.graphql.boot.test.tester;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.boot.GraphQlProperties;
import org.springframework.graphql.test.tester.GraphQlTester;
import org.springframework.graphql.test.tester.WebGraphQlTester;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.client.WebClient;
/**
* Auto-configuration for {@link GraphQlTester} in mock environments.
* Configuration classes for {@link WebGraphQlTester}
* <p>
* Those should be {@code @Import} in a regular auto-configuration class to guarantee
* their order of execution.
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({ WebClient.class, WebTestClient.class, GraphQlTester.class })
@AutoConfigureAfter(value = WebTestClientMockMvcAutoConfiguration.class,
name = "org.springframework.boot.test.autoconfigure.web.reactive.WebTestClientAutoConfiguration")
public class GraphQlTesterAutoConfiguration {
class GraphQlTesterConfiguration {
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({WebClient.class, WebTestClient.class})
@ConditionalOnBean(WebTestClient.class)
public static class WebTestClientGraphQlTesterConfiguration {
@ConditionalOnMissingBean(WebGraphQlTester.class)
public static class WebTestClientConfig {
@Bean
public WebGraphQlTester clientGraphQlTester(WebTestClient webTestClient, GraphQlProperties properties) {
public WebGraphQlTester webTestClientGraphQlTester(WebTestClient webTestClient, GraphQlProperties properties) {
WebTestClient mutatedWebTestClient = webTestClient.mutate().baseUrl(properties.getPath()).build();
return WebGraphQlTester.create(mutatedWebTestClient);
}
@@ -54,13 +52,13 @@ public class GraphQlTesterAutoConfiguration {
}
@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(WebTestClient.class)
@ConditionalOnBean(WebGraphQlHandler.class)
public static class WebGraphQlHandlerGraphQlTesterConfiguration {
@ConditionalOnMissingBean({WebGraphQlTester.class})
public static class WebGraphQlHandlerConfig {
@Bean
public WebGraphQlTester handlerGraphQlTester(WebGraphQlHandler handler) {
return WebGraphQlTester.create(handler);
public WebGraphQlTester webGraphQlTester(WebGraphQlHandler webGraphQlHandler) {
return WebGraphQlTester.create(webGraphQlHandler);
}
}

View File

@@ -0,0 +1,39 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot.test.tester;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.graphql.test.tester.GraphQlTester;
import org.springframework.graphql.test.tester.WebGraphQlTester;
/**
* Auto-configuration for {@link WebGraphQlTester}.
*
* @author Brian Clozel
* @since 1.0.0
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass({GraphQlTester.class})
@AutoConfigureAfter(value = WebTestClientMockMvcAutoConfiguration.class,
name = "org.springframework.boot.test.autoconfigure.web.reactive.WebTestClientAutoConfiguration")
@Import({GraphQlTesterConfiguration.WebTestClientConfig.class, GraphQlTesterConfiguration.WebGraphQlHandlerConfig.class})
public class WebGraphQlTesterAutoConfiguration {
}

View File

@@ -1,6 +1,6 @@
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.graphql.boot.GraphQlAutoConfiguration,\
org.springframework.graphql.boot.GraphQlServiceAutoConfiguration,\
org.springframework.graphql.boot.WebGraphQlHandlerAutoConfiguration,\
org.springframework.graphql.boot.GraphQlWebFluxAutoConfiguration,\
org.springframework.graphql.boot.GraphQlWebMvcAutoConfiguration,\
org.springframework.graphql.boot.actuate.metrics.GraphQlMetricsAutoConfiguration,\
@@ -14,10 +14,17 @@ org.springframework.boot.diagnostics.FailureAnalyzer=\
org.springframework.graphql.boot.InvalidSchemaLocationsExceptionFailureAnalyzer
# Spring Test @AutoConfigureGraphQlTester
org.springframework.graphql.boot.test.tester.AutoConfigureGraphQlTester=\
org.springframework.graphql.boot.test.tester.AutoConfigureWebGraphQlTester=\
org.springframework.graphql.boot.test.tester.WebTestClientMockMvcAutoConfiguration,\
org.springframework.graphql.boot.test.tester.GraphQlTesterAutoConfiguration
org.springframework.graphql.boot.test.tester.WebGraphQlTesterAutoConfiguration
# Spring Test ContextCustomizerFactories
org.springframework.test.context.ContextCustomizerFactory=\
org.springframework.graphql.boot.test.tester.GraphQlTesterContextCustomizerFactory
org.springframework.graphql.boot.test.tester.GraphQlTesterContextCustomizerFactory
# AutoConfigureGraphQl auto-configuration imports
org.springframework.graphql.boot.test.AutoConfigureGraphQl=\
org.springframework.boot.autoconfigure.http.codec.CodecsAutoConfiguration,\
org.springframework.boot.autoconfigure.validation.ValidationAutoConfiguration,\
org.springframework.graphql.boot.GraphQlAutoConfiguration,\
org.springframework.graphql.boot.WebGraphQlHandlerAutoConfiguration

View File

@@ -46,7 +46,7 @@ class GraphQlWebFluxAutoConfigurationTests {
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class,
CodecsAutoConfiguration.class, JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class,
GraphQlServiceAutoConfiguration.class, GraphQlWebFluxAutoConfiguration.class))
WebGraphQlHandlerAutoConfiguration.class, GraphQlWebFluxAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class)
.withPropertyValues(
"spring.main.web-application-type=reactive",

View File

@@ -50,7 +50,7 @@ class GraphQlWebMvcAutoConfigurationTests {
.withConfiguration(AutoConfigurations.of(DispatcherServletAutoConfiguration.class,
WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class,
GraphQlServiceAutoConfiguration.class, GraphQlWebMvcAutoConfiguration.class))
WebGraphQlHandlerAutoConfiguration.class, GraphQlWebMvcAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, CustomWebInterceptor.class)
.withPropertyValues(
"spring.main.web-application-type=servlet",

View File

@@ -0,0 +1,64 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.GraphQlService;
import org.springframework.graphql.data.method.annotation.support.AnnotatedControllerConfigurer;
import org.springframework.graphql.execution.BatchLoaderRegistry;
import org.springframework.graphql.execution.GraphQlSource;
import org.springframework.graphql.web.WebGraphQlHandler;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link WebGraphQlHandlerAutoConfiguration}.
*
* @author Brian Clozel
*/
class WebGraphQlHandlerAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(WebGraphQlHandlerAutoConfiguration.class))
.withUserConfiguration(GraphQlSourceConfiguration.class);
@Test
void shouldContributeStandardBeans() {
this.contextRunner.run((context) -> {
assertThat(context).hasSingleBean(BatchLoaderRegistry.class);
assertThat(context).hasSingleBean(GraphQlService.class);
assertThat(context).hasSingleBean(AnnotatedControllerConfigurer.class);
assertThat(context).hasSingleBean(WebGraphQlHandler.class);
});
}
@Configuration(proxyBeanMethods = false)
static class GraphQlSourceConfiguration {
@Bean
GraphQlSource graphQlSource() {
return mock(GraphQlSource.class);
}
}
}

View File

@@ -35,7 +35,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.boot.Book;
import org.springframework.graphql.boot.GraphQlAutoConfiguration;
import org.springframework.graphql.boot.GraphQlDataFetchers;
import org.springframework.graphql.boot.GraphQlServiceAutoConfiguration;
import org.springframework.graphql.boot.WebGraphQlHandlerAutoConfiguration;
import org.springframework.graphql.boot.GraphQlWebFluxAutoConfiguration;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
@@ -66,7 +66,7 @@ class GraphQlWebFluxSecurityAutoConfigurationTests {
private final ReactiveWebApplicationContextRunner contextRunner = new ReactiveWebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(HttpHandlerAutoConfiguration.class, WebFluxAutoConfiguration.class,
CodecsAutoConfiguration.class, JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class,
GraphQlServiceAutoConfiguration.class, GraphQlWebFluxAutoConfiguration.class,
WebGraphQlHandlerAutoConfiguration.class, GraphQlWebFluxAutoConfiguration.class,
GraphQlWebFluxSecurityAutoConfiguration.class, ReactiveSecurityAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, SecurityConfig.class)
.withPropertyValues(

View File

@@ -31,7 +31,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.boot.Book;
import org.springframework.graphql.boot.GraphQlAutoConfiguration;
import org.springframework.graphql.boot.GraphQlDataFetchers;
import org.springframework.graphql.boot.GraphQlServiceAutoConfiguration;
import org.springframework.graphql.boot.WebGraphQlHandlerAutoConfiguration;
import org.springframework.graphql.boot.GraphQlWebMvcAutoConfiguration;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
@@ -71,7 +71,7 @@ class GraphQlWebMvcSecurityAutoConfigurationTests {
.withConfiguration(AutoConfigurations.of(DispatcherServletAutoConfiguration.class,
WebMvcAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
JacksonAutoConfiguration.class, GraphQlAutoConfiguration.class,
GraphQlServiceAutoConfiguration.class, GraphQlWebMvcAutoConfiguration.class,
WebGraphQlHandlerAutoConfiguration.class, GraphQlWebMvcAutoConfiguration.class,
GraphQlWebMvcSecurityAutoConfiguration.class, SecurityAutoConfiguration.class))
.withUserConfiguration(DataFetchersConfiguration.class, SecurityConfig.class)
.withPropertyValues(

View File

@@ -0,0 +1,38 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot.test;
import reactor.core.publisher.Mono;
import org.springframework.graphql.boot.Book;
import org.springframework.graphql.data.method.annotation.Argument;
import org.springframework.graphql.data.method.annotation.QueryMapping;
import org.springframework.stereotype.Controller;
/**
* Example {@code @Controller} to be tested with {@link GraphQlTest @GraphQlTest}.
*
* @author Brian Clozel
*/
@Controller
public class BookController {
@QueryMapping
public Book bookById(@Argument String id) {
return new Book("42", "Sample Book", 100, "Jane Spring");
}
}

View File

@@ -0,0 +1,30 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot.test;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* Example {@link SpringBootApplication @SpringBootApplication} used with
* {@link GraphQlTest @GraphQlTest} tests.
*
* @author Brian Clozel
*/
@SpringBootApplication
public class ExampleGraphQlApplication {
}

View File

@@ -0,0 +1,50 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot.test;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.graphql.test.tester.WebGraphQlTester;
/**
* Integration test for {@link GraphQlTest @GraphQlTest} annotated tests.
*
* @author Brian Clozel
*/
@GraphQlTest(controllers = BookController.class,
properties = {"spring.graphql.schema.locations:classpath:books/"})
public class GraphQlTestIntegrationTest {
@Autowired
private WebGraphQlTester graphQlTester;
@Test
void getBookdByIdShouldReturnTestBook() {
String query = "{" +
" bookById(id: \"book-1\"){ " +
" id" +
" name" +
" pageCount" +
" author" +
" }" +
"}";
graphQlTester.query(query).execute()
.path("data.bookById.id").entity(String.class).isEqualTo("42");
}
}

View File

@@ -0,0 +1,184 @@
/*
* Copyright 2020-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.graphql.boot.test;
import java.io.IOException;
import com.fasterxml.jackson.databind.module.SimpleModule;
import graphql.schema.idl.RuntimeWiring;
import org.junit.jupiter.api.Test;
import reactor.core.publisher.Mono;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.core.type.classreading.MetadataReader;
import org.springframework.core.type.classreading.MetadataReaderFactory;
import org.springframework.core.type.classreading.SimpleMetadataReaderFactory;
import org.springframework.graphql.execution.RuntimeWiringConfigurer;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.graphql.web.WebInput;
import org.springframework.graphql.web.WebInterceptor;
import org.springframework.graphql.web.WebInterceptorChain;
import org.springframework.graphql.web.WebOutput;
import org.springframework.stereotype.Controller;
import org.springframework.stereotype.Repository;
import org.springframework.stereotype.Service;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link GraphQlTypeExcludeFilter}
*
* @author Brian Clozel
*/
class GraphQlTypeExcludeFilterTests {
private MetadataReaderFactory metadataReaderFactory = new SimpleMetadataReaderFactory();
@Test
void matchWhenHasNoControllers() throws Exception {
GraphQlTypeExcludeFilter filter = new GraphQlTypeExcludeFilter(WithNoControllers.class);
assertThat(excludes(filter, Controller1.class)).isFalse();
assertThat(excludes(filter, Controller2.class)).isFalse();
assertThat(excludes(filter, ExampleRuntimeWiringConfigurer.class)).isFalse();
assertThat(excludes(filter, ExampleService.class)).isTrue();
assertThat(excludes(filter, ExampleRepository.class)).isTrue();
assertThat(excludes(filter, ExampleWebInterceptor.class)).isFalse();
assertThat(excludes(filter, ExampleModule.class)).isFalse();
}
@Test
void matchWhenHasController() throws Exception {
GraphQlTypeExcludeFilter filter = new GraphQlTypeExcludeFilter(WithController.class);
assertThat(excludes(filter, Controller1.class)).isFalse();
assertThat(excludes(filter, Controller2.class)).isTrue();
assertThat(excludes(filter, ExampleRuntimeWiringConfigurer.class)).isFalse();
assertThat(excludes(filter, ExampleService.class)).isTrue();
assertThat(excludes(filter, ExampleRepository.class)).isTrue();
assertThat(excludes(filter, ExampleWebInterceptor.class)).isFalse();
assertThat(excludes(filter, ExampleModule.class)).isFalse();
}
@Test
void matchNotUsingDefaultFilters() throws Exception {
GraphQlTypeExcludeFilter filter = new GraphQlTypeExcludeFilter(NotUsingDefaultFilters.class);
assertThat(excludes(filter, Controller1.class)).isTrue();
assertThat(excludes(filter, Controller2.class)).isTrue();
assertThat(excludes(filter, ExampleRuntimeWiringConfigurer.class)).isTrue();
assertThat(excludes(filter, ExampleService.class)).isTrue();
assertThat(excludes(filter, ExampleRepository.class)).isTrue();
assertThat(excludes(filter, ExampleWebInterceptor.class)).isTrue();
assertThat(excludes(filter, ExampleModule.class)).isTrue();
}
@Test
void matchWithIncludeFilter() throws Exception {
GraphQlTypeExcludeFilter filter = new GraphQlTypeExcludeFilter(WithIncludeFilter.class);
assertThat(excludes(filter, Controller1.class)).isFalse();
assertThat(excludes(filter, Controller2.class)).isFalse();
assertThat(excludes(filter, ExampleRuntimeWiringConfigurer.class)).isFalse();
assertThat(excludes(filter, ExampleService.class)).isTrue();
assertThat(excludes(filter, ExampleRepository.class)).isFalse();
assertThat(excludes(filter, ExampleWebInterceptor.class)).isFalse();
assertThat(excludes(filter, ExampleModule.class)).isFalse();
}
@Test
void matchWithExcludeFilter() throws Exception {
GraphQlTypeExcludeFilter filter = new GraphQlTypeExcludeFilter(WithExcludeFilter.class);
assertThat(excludes(filter, Controller1.class)).isTrue();
assertThat(excludes(filter, Controller2.class)).isFalse();
assertThat(excludes(filter, ExampleRuntimeWiringConfigurer.class)).isFalse();
assertThat(excludes(filter, ExampleService.class)).isTrue();
assertThat(excludes(filter, ExampleRepository.class)).isTrue();
assertThat(excludes(filter, ExampleWebInterceptor.class)).isFalse();
assertThat(excludes(filter, ExampleModule.class)).isFalse();
}
private boolean excludes(GraphQlTypeExcludeFilter filter, Class<?> type) throws IOException {
MetadataReader metadataReader = this.metadataReaderFactory.getMetadataReader(type.getName());
return filter.match(metadataReader, this.metadataReaderFactory);
}
@GraphQlTest
static class WithNoControllers {
}
@GraphQlTest(Controller1.class)
static class WithController {
}
@GraphQlTest(useDefaultFilters = false)
static class NotUsingDefaultFilters {
}
@GraphQlTest(includeFilters = @ComponentScan.Filter(Repository.class))
static class WithIncludeFilter {
}
@GraphQlTest(excludeFilters = @ComponentScan.Filter(type = FilterType.ASSIGNABLE_TYPE, classes = Controller1.class))
static class WithExcludeFilter {
}
@Controller
static class Controller1 {
}
@Controller
static class Controller2 {
}
@Service
static class ExampleService {
}
@Repository
static class ExampleRepository {
}
static class ExampleRuntimeWiringConfigurer implements RuntimeWiringConfigurer {
@Override
public void configure(RuntimeWiring.Builder builder) {
}
}
static class ExampleWebInterceptor implements WebInterceptor {
@Override
public Mono<WebOutput> intercept(WebInput webInput, WebInterceptorChain chain) {
return null;
}
}
@SuppressWarnings("serial")
static class ExampleModule extends SimpleModule {
}
}

View File

@@ -23,7 +23,7 @@ import org.springframework.boot.test.context.runner.ApplicationContextRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.graphql.boot.GraphQlProperties;
import org.springframework.graphql.test.tester.GraphQlTester;
import org.springframework.graphql.test.tester.WebGraphQlTester;
import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.test.web.reactive.server.WebTestClient;
import org.springframework.web.reactive.function.server.RouterFunction;
@@ -34,20 +34,20 @@ import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link GraphQlTesterAutoConfiguration}
* Tests for {@link WebGraphQlTesterAutoConfiguration}
*
* @author Brian Clozel
*/
class GraphQlTesterAutoConfigurationTests {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(GraphQlTesterAutoConfiguration.class));
.withConfiguration(AutoConfigurations.of(WebGraphQlTesterAutoConfiguration.class));
@Test
void shouldNotBeConfiguredWithoutGraphQlHandlerNorWebTestClient() {
this.contextRunner.run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context).doesNotHaveBean(GraphQlTester.class);
assertThat(context).doesNotHaveBean(WebGraphQlTester.class);
});
}
@@ -55,7 +55,7 @@ class GraphQlTesterAutoConfigurationTests {
void shouldBeConfiguredWhenGraphQlHandlerPresent() {
this.contextRunner.withUserConfiguration(HandlerConfiguration.class).run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context).hasSingleBean(GraphQlTester.class);
assertThat(context).hasSingleBean(WebGraphQlTester.class);
});
}
@@ -63,7 +63,7 @@ class GraphQlTesterAutoConfigurationTests {
void shouldBeConfiguredWhenWebTestClientPresent() {
this.contextRunner.withUserConfiguration(WebTestClientConfiguration.class).run((context) -> {
assertThat(context).hasNotFailed();
assertThat(context).hasSingleBean(GraphQlTester.class);
assertThat(context).hasSingleBean(WebGraphQlTester.class);
});
}