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

@@ -48,6 +48,7 @@ dependencies {
compileOnly project(':spring-graphql-test')
compileOnly 'org.junit.jupiter:junit-jupiter-api'
compileOnly 'org.springframework.boot:spring-boot-test'
compileOnly 'org.springframework.boot:spring-boot-test-autoconfigure'
compileOnly 'com.google.code.findbugs:jsr305'
annotationProcessor 'org.springframework.boot:spring-boot-configuration-processor'

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);
});
}

View File

@@ -20,7 +20,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.reactive.AutoConfigureWebTestClient;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.boot.test.tester.AutoConfigureGraphQlTester;
import org.springframework.graphql.boot.test.tester.AutoConfigureWebGraphQlTester;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.graphql.test.tester.WebGraphQlTester;
@@ -29,7 +29,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
@SpringBootTest()
@AutoConfigureWebTestClient
@AutoConfigureGraphQlTester
@AutoConfigureWebGraphQlTester
class SampleApplicationTests {
@Autowired

View File

@@ -4,7 +4,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.boot.test.tester.AutoConfigureGraphQlTester;
import org.springframework.graphql.boot.test.tester.AutoConfigureWebGraphQlTester;
import org.springframework.graphql.execution.ErrorType;
import org.springframework.graphql.test.tester.WebGraphQlTester;
@@ -13,7 +13,7 @@ import static org.assertj.core.api.Assertions.assertThatThrownBy;
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureGraphQlTester
@AutoConfigureWebGraphQlTester
class SampleApplicationTests {
@Autowired

View File

@@ -20,7 +20,7 @@ import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.graphql.boot.test.tester.AutoConfigureGraphQlTester;
import org.springframework.graphql.boot.test.tester.AutoConfigureWebGraphQlTester;
import org.springframework.graphql.test.tester.GraphQlTester;
import org.springframework.test.web.servlet.MockMvc;
@@ -31,7 +31,7 @@ import static org.assertj.core.api.Assertions.assertThat;
*/
@SpringBootTest
@AutoConfigureMockMvc
@AutoConfigureGraphQlTester
@AutoConfigureWebGraphQlTester
public class MockMvcGraphQlTests {
@Autowired

View File

@@ -119,7 +119,7 @@ spring.graphql.schema.printer.enabled=false
The GraphQL Java `RuntimeWiring.Builder` can be used to register ``DataFetcher``s,
type resolvers, custom scalar types, and more. You can declare `RuntimeWiringConfigurer`
beans in your Spring config to get access to the `RuntimeWiring.Builder`. The Boot
starter detects such beans adds them to <<index#execution-graphqlsource,GraphQlSource.Builder>>.
starter detects such beans and adds them to <<index#execution-graphqlsource,GraphQlSource.Builder>>.
Typically, however, applications will not implement ``DataFetcher`` directly and will
instead create <<index#controllers,annotated controllers>>. The Boot
@@ -364,8 +364,12 @@ A GraphQL error metric counter is available at `/actuator/metrics/graphql.error`
[[boot-graphql-testing]]
== Testing
For Spring GraphQL testing support, add the below to your classpath and that will make
a `WebGraphQlTester` available for injection into tests:
Spring GraphQL offers many ways to test your application: with or without a live server, using the transport
or testing directly the engine. You'll be using a lot the <<testing#testing-webgraphqltester,WebGraphQlTester>>,
so make sure you're familiar with it before writing your first test.
The Spring Boot starter will help you and configure the testing infrastructure; all you to start
is to add the following to your classpath:
[source,groovy,indent=0,subs="verbatim,quotes,attributes",role="primary"]
.Gradle
@@ -374,7 +378,7 @@ dependencies {
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.graphql:spring-graphql-test:{spring-graphql-version}'
// Also add this, unless `spring-boot-starter-webflux` is also present
// Also add this, unless spring-boot-starter-webflux is also present
testImplementation 'org.springframework:spring-webflux'
// ...
@@ -432,6 +436,89 @@ repositories {
</repositories>
----
In the next sections, we'll see the various options available for testing your Spring GraphQL application.
[[boot-graphql-testing-graphqltest]]
=== Testing GraphQL components only
You can test your Spring GraphQL `@Controller` and GraphQL components with the `@GraphQlTest` annotation.
`@GraphQlTest` auto-configures the Spring GraphQL infrastructure ad limits scanned beans to `@Controller`,
`RuntimeWiringConfigurer`, `JsonComponent`, `WebInterceptor`, `Converter`, `GenericConverter`
Regular `@Component` and `@ConfigurationProperties` beans are not scanned when the `@GraphQlTest` annotation is used.
`@EnableConfigurationProperties` can be used to include `@ConfigurationProperties` beans.
This arrangement is quite similar to the
{spring-boot-ref-docs}/features.html#features.testing.spring-boot-applications.spring-mvc-tests[@WevMvcTest support],
except that the web framework of choice (Spring MVC or Spring WebFlux) is not involved at all as requests are performed
directly against the `WebGraphQlHandler`.
[source,java,indent=0,subs="verbatim,quotes"]
----
@GraphQlTest(controllers = BookController.class)
public class BookControllerTests {
@Autowired
private WebGraphQlTester graphQlTester;
@MockBean
private BookRepository bookRepository;
@Test
void bookdByIdShouldReturnSpringBook() {
given(this.bookRepository.findById(42L)).willReturn(new Book(42L, "Spring GraphQL"));
String query = //
graphQlTester.query(query).execute()
.path("data.bookById.name").entity(String.class).isEqualTo("Spring GraphQL");
}
}
----
This mode is also useful for testing subscriptions without involving the transport protocol.
[source,java,indent=0,subs="verbatim,quotes"]
----
@GraphQlTest(controllers = GreetingController.class)
public class GreetingControllerTests {
@Autowired
private WebGraphQlTester graphQlTester;
@Test
void subscription() {
Flux<String> result = this.graphQlTester.query("subscription { greetings }")
.executeSubscription()
.toFlux("greetings", String.class);
// Use StepVerifier from "reactor-test" to verify the stream...
StepVerifier.create(result)
.expectNext("Hi")
.expectNext("Bonjour")
.expectNext("Hola")
.verifyComplete();
}
}
----
The above subscription test is performed directly against the `WebGraphQlHandler` that
both HTTP and WebSocket transports delegate to. It passes through the `WebInterceptor`
chain and then calls GraphQL Java which returns a Reactive Streams `Publisher`.
[NOTE]
.Testing multiple controllers with `@GraphQlTest`
====
Because GraphQL is not about REST endpoints but navigating relations in an object graph,
multiple `@Controller` components can be involved in a single query.
For this case, `@GraphQlTest` supports testing multiple controllers with its `controllers` annotation attribute.
====
[[boot-graphql-testing-mock]]
=== Testing the HTTP transport with a Mock server
If your test requires more integration with application components, you can choose to test the entire
application and involve the transport layers and the Web framework.
For GraphQL over HTTP with Spring MVC, using `MockMvc` as the server:
[source,java,indent=0,subs="verbatim,quotes"]
@@ -463,13 +550,18 @@ public class MockMvcGraphQlTests {
}
----
For GraphQL over HTTP with a
{spring-boot-ref-docs}/features.html#features.testing.spring-boot-applications.with-running-server[running server]:
[[boot-graphql-testing-live]]
=== Testing the HTTP transport with a live server
You can also run tests against the full application infrastructure, including a live server.
Just like {spring-boot-ref-docs}/features.html#features.testing.spring-boot-applications.with-running-server[REST endpoints testing],
you can use a `WebEnvironment.RANDOM_PORT` environment and test queries using `WebGraphQlTester`.
[source,java,indent=0,subs="verbatim,quotes"]
----
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
@AutoConfigureGraphQlTester
public class MockMvcGraphQlTests {
@Autowired
@@ -477,35 +569,3 @@ public class MockMvcGraphQlTests {
}
----
Subscriptions can be tested without WebSocket as shown below:
[source,java,indent=0,subs="verbatim,quotes"]
----
@SpringBootTest
@AutoConfigureGraphQlTester
public class MockMvcGraphQlTests {
@Autowired
private WebGraphQlTester graphQlTester;
@Test
void subscription() {
Flux<String> result = this.graphQlTester.query("subscription { greetings }")
.executeSubscription()
.toFlux("greetings", String.class);
// Use StepVerifier from "reactor-test" to verify the stream...
StepVerifier.create(result)
.expectNext("Hi")
.expectNext("Bonjour")
.expectNext("Hola")
.verifyComplete();
}
}
----
The above subscription test is performed directly against the `WebGraphQlHandler` that
both HTTP and WebSocket transports delegate to. It passes through the `WebInterceptor`
chain and then calls GraphQL Java which returns a Reactive Streams `Publisher`.

View File

@@ -24,8 +24,8 @@ import org.springframework.test.web.reactive.server.WebTestClient;
/**
* Main entry point for testing GraphQL over a Web transport with requests
* executed {@link #create(WebTestClient) via WebTestClient} or
* {@link #create(WebGraphQlHandler) via WebGraphQlHandler}. See the below for
* examples with different scenarios.
* {@link #create(WebGraphQlHandler) via WebGraphQlHandler}.
*
*
* <p>
* GraphQL requests to Spring MVC without an HTTP server: <pre class="code">