From de234c94c49a760da7004504e63292345fd23632 Mon Sep 17 00:00:00 2001 From: Rossen Stoyanchev Date: Mon, 14 Jun 2021 14:21:02 +0100 Subject: [PATCH] Decouple GraphQlTester from Web details Closes gh-65 --- .../GraphQlTesterAutoConfiguration.java | 9 +- .../GraphQlTesterContextCustomizer.java | 5 +- .../io/spring/sample/graphql/QueryTests.java | 3 +- .../sample/graphql/SubscriptionTests.java | 3 +- .../test/tester/DefaultGraphQlTester.java | 126 +++----- .../test/tester/DefaultWebGraphQlTester.java | 122 ++++++++ .../graphql/test/tester/GraphQlTester.java | 88 +----- .../graphql/test/tester/WebGraphQlTester.java | 104 ++++++ .../test/tester/GraphQlTesterTests.java | 296 +++++------------- .../test/tester/WebGraphQlTesterTests.java | 216 +++++++++++++ 10 files changed, 589 insertions(+), 383 deletions(-) create mode 100644 spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java create mode 100644 spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlTester.java create mode 100644 spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterTests.java diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/test/tester/GraphQlTesterAutoConfiguration.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/test/tester/GraphQlTesterAutoConfiguration.java index b5671d72..4e1f5e51 100644 --- a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/test/tester/GraphQlTesterAutoConfiguration.java +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/test/tester/GraphQlTesterAutoConfiguration.java @@ -24,6 +24,7 @@ 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; @@ -45,9 +46,9 @@ public class GraphQlTesterAutoConfiguration { public static class WebTestClientGraphQlTesterConfiguration { @Bean - public GraphQlTester clientGraphQlTester(WebTestClient webTestClient, GraphQlProperties properties) { + public WebGraphQlTester clientGraphQlTester(WebTestClient webTestClient, GraphQlProperties properties) { WebTestClient mutatedWebTestClient = webTestClient.mutate().baseUrl(properties.getPath()).build(); - return GraphQlTester.create(mutatedWebTestClient); + return WebGraphQlTester.create(mutatedWebTestClient); } } @@ -58,8 +59,8 @@ public class GraphQlTesterAutoConfiguration { public static class WebGraphQlHandlerGraphQlTesterConfiguration { @Bean - public GraphQlTester handlerGraphQlTester(WebGraphQlHandler handler) { - return GraphQlTester.create(handler); + public WebGraphQlTester handlerGraphQlTester(WebGraphQlHandler handler) { + return WebGraphQlTester.create(handler); } } diff --git a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/test/tester/GraphQlTesterContextCustomizer.java b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/test/tester/GraphQlTesterContextCustomizer.java index 57145276..5b9faf94 100644 --- a/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/test/tester/GraphQlTesterContextCustomizer.java +++ b/graphql-spring-boot-starter/src/main/java/org/springframework/graphql/boot/test/tester/GraphQlTesterContextCustomizer.java @@ -35,6 +35,7 @@ import org.springframework.context.ApplicationContextAware; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.core.Ordered; import org.springframework.graphql.test.tester.GraphQlTester; +import org.springframework.graphql.test.tester.WebGraphQlTester; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.MergedContextConfiguration; import org.springframework.test.context.TestContextAnnotationUtils; @@ -142,12 +143,12 @@ class GraphQlTesterContextCustomizer implements ContextCustomizer { return this.object; } - private GraphQlTester createGraphQlTester() { + private WebGraphQlTester createGraphQlTester() { WebTestClient webTestClient = this.applicationContext.getBean(WebTestClient.class); boolean sslEnabled = isSslEnabled(this.applicationContext); String port = this.applicationContext.getEnvironment().getProperty("local.server.port", "8080"); WebTestClient mutatedWebClient = webTestClient.mutate().baseUrl(getBaseUrl(sslEnabled, port)).build(); - return GraphQlTester.create(mutatedWebClient); + return WebGraphQlTester.create(mutatedWebClient); } private String getBaseUrl(boolean sslEnabled, String port) { diff --git a/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/QueryTests.java b/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/QueryTests.java index 972c3386..8e0b83d8 100644 --- a/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/QueryTests.java +++ b/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/QueryTests.java @@ -22,6 +22,7 @@ import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.graphql.test.tester.GraphQlTester; +import org.springframework.graphql.test.tester.WebGraphQlTester; import org.springframework.graphql.web.WebGraphQlHandler; // @formatter:off @@ -36,7 +37,7 @@ public class QueryTests { @BeforeEach public void setUp(@Autowired WebGraphQlHandler handler) { - this.graphQlTester = GraphQlTester.create(webInput -> + this.graphQlTester = WebGraphQlTester.create(webInput -> handler.handle(webInput).contextWrite(context -> context.put("name", "James"))); } diff --git a/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/SubscriptionTests.java b/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/SubscriptionTests.java index 69336e63..d610d3db 100644 --- a/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/SubscriptionTests.java +++ b/samples/webflux-websocket/src/test/java/io/spring/sample/graphql/SubscriptionTests.java @@ -24,6 +24,7 @@ import reactor.test.StepVerifier; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.graphql.test.tester.GraphQlTester; +import org.springframework.graphql.test.tester.WebGraphQlTester; import org.springframework.graphql.web.WebGraphQlHandler; // @formatter:off @@ -38,7 +39,7 @@ public class SubscriptionTests { @BeforeEach public void setUp(@Autowired WebGraphQlHandler handler) { - this.graphQlTester = GraphQlTester.create(webInput -> + this.graphQlTester = WebGraphQlTester.create(webInput -> handler.handle(webInput).contextWrite(context -> context.put("name", "James"))); } diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java index d1aef807..e001a4de 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java @@ -16,8 +16,6 @@ package org.springframework.graphql.test.tester; -import java.net.URI; -import java.nio.charset.StandardCharsets; import java.time.Duration; import java.util.ArrayList; import java.util.Arrays; @@ -37,24 +35,17 @@ import com.jayway.jsonpath.TypeRef; import com.jayway.jsonpath.spi.json.JacksonJsonProvider; import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider; import graphql.ExecutionResult; -import graphql.GraphQL; import graphql.GraphQLError; import org.reactivestreams.Publisher; import reactor.core.publisher.Flux; import org.springframework.core.ParameterizedTypeReference; +import org.springframework.graphql.GraphQlService; import org.springframework.graphql.RequestInput; -import org.springframework.graphql.web.WebGraphQlHandler; -import org.springframework.graphql.web.WebInput; -import org.springframework.http.HttpHeaders; -import org.springframework.http.MediaType; import org.springframework.lang.Nullable; import org.springframework.test.util.AssertionErrors; import org.springframework.test.util.JsonExpectationsHelper; import org.springframework.test.util.JsonPathExpectationsHelper; -import org.springframework.test.web.reactive.server.EntityExchangeResult; -import org.springframework.test.web.reactive.server.FluxExchangeResult; -import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; import org.springframework.util.CollectionUtils; @@ -77,19 +68,16 @@ class DefaultGraphQlTester implements GraphQlTester { private final RequestStrategy requestStrategy; - private final Configuration jsonPathConfig; - DefaultGraphQlTester(WebTestClient client) { - this.jsonPathConfig = initJsonPathConfig(); - this.requestStrategy = new WebTestClientRequestStrategy(client, this.jsonPathConfig); + DefaultGraphQlTester(GraphQlService service) { + this(new GraphQlServiceRequestStrategy(service, initJsonPathConfig())); } - DefaultGraphQlTester(WebGraphQlHandler handler) { - this.jsonPathConfig = initJsonPathConfig(); - this.requestStrategy = new DirectRequestStrategy(handler, this.jsonPathConfig); + DefaultGraphQlTester(RequestStrategy requestStrategy) { + this.requestStrategy = requestStrategy; } - private Configuration initJsonPathConfig() { + protected static Configuration initJsonPathConfig() { return (jackson2Present ? Jackson2Configuration.create() : Configuration.builder().build()); } @@ -120,65 +108,17 @@ class DefaultGraphQlTester implements GraphQlTester { } /** - * {@link RequestStrategy} that works as an HTTP client with requests executed through - * {@link WebTestClient} that in turn may work connect with or without a live server - * for Spring MVC and WebFlux. + * Base class for a {@link RequestStrategy} that perform GraphQL requests + * without an underlying transport and where {@link RequestInput} provides + * sufficient input. */ - private static class WebTestClientRequestStrategy implements RequestStrategy { + protected abstract static class AbstractDirectRequestStrategy implements RequestStrategy { - private final WebTestClient client; + protected static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5); private final Configuration jsonPathConfig; - WebTestClientRequestStrategy(WebTestClient client, Configuration jsonPathConfig) { - this.client = client; - this.jsonPathConfig = jsonPathConfig; - } - - @Override - public ResponseSpec execute(RequestInput requestInput) { - EntityExchangeResult result = this.client.post().contentType(MediaType.APPLICATION_JSON) - .bodyValue(requestInput).exchange().expectStatus().isOk().expectHeader() - .contentType(MediaType.APPLICATION_JSON).expectBody().returnResult(); - - byte[] bytes = result.getResponseBodyContent(); - Assert.notNull(bytes, "Expected GraphQL response content"); - String content = new String(bytes, StandardCharsets.UTF_8); - DocumentContext documentContext = JsonPath.parse(content, this.jsonPathConfig); - - return new DefaultResponseSpec(documentContext, result::assertWithDiagnostics); - } - - @Override - public SubscriptionSpec executeSubscription(RequestInput requestInput) { - FluxExchangeResult exchangeResult = this.client.post() - .contentType(MediaType.APPLICATION_JSON).accept(MediaType.TEXT_EVENT_STREAM).bodyValue(requestInput) - .exchange().expectStatus().isOk().expectHeader().contentType(MediaType.TEXT_EVENT_STREAM) - .returnResult(TestExecutionResult.class); - - return new DefaultSubscriptionSpec(exchangeResult.getResponseBody().cast(ExecutionResult.class), - this.jsonPathConfig, exchangeResult::assertWithDiagnostics); - } - - } - - /** - * {@link RequestStrategy} that performs requests directly on {@link GraphQL}. - */ - private static class DirectRequestStrategy implements RequestStrategy { - - private static final URI DEFAULT_URL = URI.create("http://localhost:8080/graphql"); - - private static final HttpHeaders DEFAULT_HEADERS = new HttpHeaders(); - - private static final Duration DEFAULT_TIMEOUT = Duration.ofSeconds(5); - - private final WebGraphQlHandler graphQlHandler; - - private final Configuration jsonPathConfig; - - DirectRequestStrategy(WebGraphQlHandler handler, Configuration jsonPathConfig) { - this.graphQlHandler = handler; + protected AbstractDirectRequestStrategy(Configuration jsonPathConfig) { this.jsonPathConfig = jsonPathConfig; } @@ -196,19 +136,16 @@ class DefaultGraphQlTester implements GraphQlTester { List errors = result.getErrors(); Consumer assertDecorator = assertDecorator(input); - assertDecorator - .accept(() -> AssertionErrors.assertTrue("Response has " + errors.size() + " unexpected error(s).", - CollectionUtils.isEmpty(errors))); + assertDecorator.accept(() -> AssertionErrors.assertTrue( + "Response has " + errors.size() + " unexpected error(s).", CollectionUtils.isEmpty(errors))); return new DefaultSubscriptionSpec(result.getData(), this.jsonPathConfig, assertDecorator); } - private ExecutionResult executeInternal(RequestInput input) { - WebInput webInput = new WebInput(DEFAULT_URL, DEFAULT_HEADERS, input.toMap(), null); - ExecutionResult result = this.graphQlHandler.handle(webInput).block(DEFAULT_TIMEOUT); - Assert.notNull(result, "Expected ExecutionResult"); - return result; - } + /** + * Sub-classes implement this to actual perform the request. + */ + protected abstract ExecutionResult executeInternal(RequestInput input); private Consumer assertDecorator(RequestInput input) { return (assertion) -> { @@ -223,6 +160,25 @@ class DefaultGraphQlTester implements GraphQlTester { } + /** + * {@link RequestStrategy} that performs requests through a {@link GraphQlService}. + */ + protected static class GraphQlServiceRequestStrategy extends AbstractDirectRequestStrategy { + + private final GraphQlService graphQlService; + + protected GraphQlServiceRequestStrategy(GraphQlService service, Configuration jsonPathConfig) { + super(jsonPathConfig); + this.graphQlService = service; + } + + protected ExecutionResult executeInternal(RequestInput input) { + ExecutionResult result = this.graphQlService.execute(input.toExecutionInput()).block(DEFAULT_TIMEOUT); + Assert.notNull(result, "Expected ExecutionResult"); + return result; + } + } + /** * {@link RequestSpec} that collects the query, operationName, and variables. */ @@ -375,7 +331,7 @@ class DefaultGraphQlTester implements GraphQlTester { /** * {@link ResponseSpec} that operates on the response from a GraphQL HTTP request. */ - private static final class DefaultResponseSpec implements ResponseSpec, ErrorSpec { + protected static final class DefaultResponseSpec implements ResponseSpec, ErrorSpec { private final ResponseContainer responseContainer; @@ -385,7 +341,7 @@ class DefaultGraphQlTester implements GraphQlTester { * @param assertDecorator decorator to apply around assertions, e.g. to add extra * contextual information such as HTTP request and response body details */ - private DefaultResponseSpec(DocumentContext documentContext, Consumer assertDecorator) { + protected DefaultResponseSpec(DocumentContext documentContext, Consumer assertDecorator) { this.responseContainer = new ResponseContainer(documentContext, assertDecorator); } @@ -714,7 +670,7 @@ class DefaultGraphQlTester implements GraphQlTester { * {@link SubscriptionSpec} implementation that operates on a {@link Publisher} of * {@link ExecutionResult}. */ - private static class DefaultSubscriptionSpec implements SubscriptionSpec { + protected static class DefaultSubscriptionSpec implements SubscriptionSpec { private final Publisher publisher; @@ -722,7 +678,7 @@ class DefaultGraphQlTester implements GraphQlTester { private final Consumer assertDecorator; - DefaultSubscriptionSpec(Publisher publisher, Configuration jsonPathConfig, + protected DefaultSubscriptionSpec(Publisher publisher, Configuration jsonPathConfig, Consumer decorator) { this.publisher = publisher; diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java new file mode 100644 index 00000000..03a55af5 --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTester.java @@ -0,0 +1,122 @@ +/* + * Copyright 2002-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.test.tester; + +import java.net.URI; +import java.nio.charset.StandardCharsets; + +import com.jayway.jsonpath.Configuration; +import com.jayway.jsonpath.DocumentContext; +import com.jayway.jsonpath.JsonPath; +import graphql.ExecutionResult; + +import org.springframework.graphql.RequestInput; +import org.springframework.graphql.web.WebGraphQlHandler; +import org.springframework.graphql.web.WebInput; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.test.web.reactive.server.EntityExchangeResult; +import org.springframework.test.web.reactive.server.FluxExchangeResult; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.Assert; + +/** + * Default implementation of {@link WebGraphQlTester}. + * + * @author Rossen Stoyanchev + */ +class DefaultWebGraphQlTester extends DefaultGraphQlTester implements WebGraphQlTester { + + + DefaultWebGraphQlTester(WebTestClient client) { + super(new WebTestClientRequestStrategy(client, initJsonPathConfig())); + } + + DefaultWebGraphQlTester(WebGraphQlHandler handler) { + super(new WebGraphQlHandlerRequestStrategy(handler, initJsonPathConfig())); + } + + + /** + * {@link RequestStrategy} that works as an HTTP client with requests executed through + * {@link WebTestClient} that in turn may work connect with or without a live server + * for Spring MVC and WebFlux. + */ + private static class WebTestClientRequestStrategy implements RequestStrategy { + + private final WebTestClient client; + + private final Configuration jsonPathConfig; + + WebTestClientRequestStrategy(WebTestClient client, Configuration jsonPathConfig) { + this.client = client; + this.jsonPathConfig = jsonPathConfig; + } + + @Override + public ResponseSpec execute(RequestInput requestInput) { + EntityExchangeResult result = this.client.post().contentType(MediaType.APPLICATION_JSON) + .bodyValue(requestInput).exchange().expectStatus().isOk().expectHeader() + .contentType(MediaType.APPLICATION_JSON).expectBody().returnResult(); + + byte[] bytes = result.getResponseBodyContent(); + Assert.notNull(bytes, "Expected GraphQL response content"); + String content = new String(bytes, StandardCharsets.UTF_8); + DocumentContext documentContext = JsonPath.parse(content, this.jsonPathConfig); + + return new DefaultResponseSpec(documentContext, result::assertWithDiagnostics); + } + + @Override + public SubscriptionSpec executeSubscription(RequestInput requestInput) { + FluxExchangeResult exchangeResult = this.client.post() + .contentType(MediaType.APPLICATION_JSON).accept(MediaType.TEXT_EVENT_STREAM).bodyValue(requestInput) + .exchange().expectStatus().isOk().expectHeader().contentType(MediaType.TEXT_EVENT_STREAM) + .returnResult(TestExecutionResult.class); + + return new DefaultSubscriptionSpec(exchangeResult.getResponseBody().cast(ExecutionResult.class), + this.jsonPathConfig, exchangeResult::assertWithDiagnostics); + } + + } + + /** + * {@link RequestStrategy} that performs requests directly on + * {@link WebGraphQlHandler}, i.e. Web request testing without a transport. + */ + private static class WebGraphQlHandlerRequestStrategy extends AbstractDirectRequestStrategy { + + private static final URI DEFAULT_URL = URI.create("http://localhost:8080/graphql"); + + private static final HttpHeaders DEFAULT_HEADERS = new HttpHeaders(); + + private final WebGraphQlHandler graphQlHandler; + + WebGraphQlHandlerRequestStrategy(WebGraphQlHandler handler, Configuration jsonPathConfig) { + super(jsonPathConfig); + this.graphQlHandler = handler; + } + + protected ExecutionResult executeInternal(RequestInput input) { + WebInput webInput = new WebInput(DEFAULT_URL, DEFAULT_HEADERS, input.toMap(), null); + ExecutionResult result = this.graphQlHandler.handle(webInput).block(DEFAULT_TIMEOUT); + Assert.notNull(result, "Expected ExecutionResult"); + return result; + } + } + +} diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java index d80a0402..1461d698 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java @@ -25,69 +25,17 @@ import graphql.GraphQLError; import reactor.core.publisher.Flux; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.graphql.web.WebGraphQlHandler; +import org.springframework.graphql.GraphQlService; import org.springframework.lang.Nullable; -import org.springframework.test.web.reactive.server.WebTestClient; /** - * Main entry point for testing GraphQL with requests performed either as an HTTP client - * via {@link WebTestClient} or directly via a {@link WebGraphQlHandler}. + * Contract for testing GraphQL requests. * - * - *

- * GraphQL requests to Spring MVC without an HTTP server:

- * @SpringBootTest
- * @AutoConfigureMockMvc
- * public class MyTests {
- *
- *  private GraphQlTester graphQlTester;
- *
- *  @BeforeEach
- *  public void setUp(@Autowired MockMvc mockMvc) {
- *      WebTestClient client = MockMvcWebTestClient.bindTo(mockMvc).baseUrl("/graphql").build();
- *      this.graphQlTester = GraphQlTester.create(client);
- *  }
- * 
- * - *

- * GraphQL requests to Spring WebFlux without an HTTP server:

- * @SpringBootTest
- * @AutoConfigureWebTestClient
- * public class MyTests {
- *
- *  private GraphQlTester graphQlTester;
- *
- *  @BeforeEach
- *  public void setUp(@Autowired WebTestClient client) {
- *      this.graphQlTester = GraphQlTester.create(client);
- *  }
- * 
- * - *

- * GraphQL requests to a running server:

- * @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
- * public class MyTests {
- *
- *  private GraphQlTester graphQlTester;
- *
- *  @BeforeEach
- *  public void setUp(@Autowired WebTestClient client) {
- *      this.graphQlTester = GraphQlTester.create(client);
- *  }
- * 
- * - *

- * GraphQL requests to any {@link WebGraphQlHandler}:

- * @SpringBootTest
- * public class MyTests {
- *
- *  private GraphQlTester graphQlTester;
- *
- *  @BeforeEach
- *  public void setUp(@Autowired WebGraphQLHandler handler) {
- *      this.graphQlTester = GraphQlTester.create(handler);
- *  }
- * 
+ *

The workflow declared to prepare, execute, and verify requests is not tied + * to any specific underlying transport. Use {@link WebGraphQlTester} to test + * GraphQL requests over a Web transport. This class can also be used to perform + * calls directly on {@link graphql.GraphQL}, without a transport, via + * {@link GraphQlService}. * * @author Rossen Stoyanchev * @since 1.0.0 @@ -103,28 +51,18 @@ public interface GraphQlTester { */ RequestSpec query(String query); - /** - * Create a {@code GraphQlTester} that performs GraphQL requests as an HTTP client - * through the given {@link WebTestClient}. Depending on how the {@code WebTestClient} - * is set up, tests may be with or without a server. See setup examples in class-level - * Javadoc. - * @param client the web client to perform requests with - * @return the created {@code GraphQlTester} instance - */ - static GraphQlTester create(WebTestClient client) { - return new DefaultGraphQlTester(client); - } /** * Create a {@code GraphQlTester} that performs GraphQL requests through the given - * {@link WebGraphQlHandler}. - * @param handler the handler to execute requests with - * @return the created {@code GraphQlTester} instance + * {@link GraphQlService}. + * @param service the service to execute requests with + * @return the created {@code GraphQlTester} */ - static GraphQlTester create(WebGraphQlHandler handler) { - return new DefaultGraphQlTester(handler); + static GraphQlTester create(GraphQlService service) { + return new DefaultGraphQlTester(service); } + /** * Declare options to perform a GraphQL request. */ diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlTester.java new file mode 100644 index 00000000..277e5e8b --- /dev/null +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/WebGraphQlTester.java @@ -0,0 +1,104 @@ +/* + * Copyright 2002-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.test.tester; + +import org.springframework.graphql.web.WebGraphQlHandler; +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. + * + *

+ * GraphQL requests to Spring MVC without an HTTP server:

+ * @SpringBootTest
+ * @AutoConfigureMockMvc
+ * @AutoConfigureGraphQlTester
+ * public class MyTests {
+ *
+ *   @Autowired
+ *   private WebGraphQlTester graphQlTester;
+ * }
+ * 
+ * + *

+ * GraphQL requests to Spring WebFlux without an HTTP server:

+ * @SpringBootTest
+ * @AutoConfigureWebTestClient
+ * @AutoConfigureGraphQlTester
+ * public class MyTests {
+ *
+ *   @Autowired
+ *   private WebGraphQlTester graphQlTester;
+ * }
+ * 
+ * + *

+ * GraphQL requests to a running server:

+ * @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
+ * @AutoConfigureWebTestClient
+ * @AutoConfigureGraphQlTester
+ * public class MyTests {
+ *
+ *   @Autowired
+ *   private WebGraphQlTester graphQlTester;
+ * }
+ * 
+ * + *

+ * GraphQL requests handled directly through a {@link WebGraphQlHandler}:

+ * @SpringBootTest
+ * public class MyTests {
+ *
+ *   private WebGraphQlTester graphQlTester;
+ *
+ *   @BeforeEach
+ *   public void setUp(@Autowired WebGraphQLHandler handler) {
+ *       this.graphQlTester = GraphQlTester.create(handler);
+ *   }
+ * }
+ * 
+ * + * @author Rossen Stoyanchev + * @since 1.0.0 + */ +public interface WebGraphQlTester extends GraphQlTester { + + /** + * Create a {@code WebGraphQlTester} that performs GraphQL requests as an + * HTTP client through the given {@link WebTestClient}. Depending on how the + * {@code WebTestClient} is set up, tests may be with or without a server. + * See setup examples in class-level Javadoc. + * @param client the web client to perform requests with + * @return the created {@code WebGraphQlTester} + */ + static WebGraphQlTester create(WebTestClient client) { + return new DefaultWebGraphQlTester(client); + } + + /** + * Create a {@code WebGraphQlTester} that performs GraphQL requests through + * the given {@link WebGraphQlHandler}. + * @param handler the handler to execute requests with + * @return the created {@code WebGraphQlTester} + */ + static WebGraphQlTester create(WebGraphQlHandler handler) { + return new DefaultWebGraphQlTester(handler); + } + +} diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java index a8f2829e..2c4a033a 100644 --- a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java @@ -21,34 +21,23 @@ import java.util.Collections; import java.util.List; import java.util.Map; import java.util.concurrent.atomic.AtomicReference; -import java.util.function.Consumer; -import java.util.stream.Collectors; -import java.util.stream.Stream; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; +import graphql.ExecutionInput; import graphql.ExecutionResult; import graphql.ExecutionResultImpl; import graphql.GraphQLError; import graphql.GraphqlErrorBuilder; import graphql.language.SourceLocation; -import okhttp3.mockwebserver.MockResponse; -import okhttp3.mockwebserver.MockWebServer; -import okhttp3.mockwebserver.RecordedRequest; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.api.Test; import org.mockito.ArgumentCaptor; import reactor.core.publisher.Mono; import org.springframework.core.ParameterizedTypeReference; -import org.springframework.graphql.web.WebGraphQlHandler; -import org.springframework.graphql.web.WebInput; -import org.springframework.graphql.web.WebOutput; -import org.springframework.http.HttpHeaders; +import org.springframework.graphql.GraphQlService; import org.springframework.lang.Nullable; -import org.springframework.test.web.reactive.server.WebTestClient; import org.springframework.util.CollectionUtils; -import org.springframework.util.StringUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -56,11 +45,7 @@ import static org.mockito.BDDMockito.given; import static org.mockito.Mockito.mock; /** - * Tests for {@link GraphQlTester} parameterized to: - *
    - *
  • Connect to {@link MockWebServer} and return a preset HTTP response. - *
  • Use mock {@link WebGraphQlHandler} to return a preset {@link ExecutionResult}. - *
+ * Tests for {@link GraphQlTester}. * *

* There is no actual handling via {@link graphql.GraphQL} in either scenario. The main @@ -70,35 +55,36 @@ public class GraphQlTesterTests { private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); - public static Stream argumentSource() { - return Stream.of(new MockWebServerSetup(), new MockWebGraphQlHandlerSetup()); - } - @ParameterizedTest - @MethodSource("argumentSource") - void pathAndValueExistsAndEmptyChecks(GraphQlTesterSetup setup) throws Exception { + private final GraphQlService service = mock(GraphQlService.class); + + private final ArgumentCaptor inputCaptor = ArgumentCaptor.forClass(ExecutionInput.class); + + private final GraphQlTester graphQlTester = GraphQlTester.create(this.service); + + + @Test + void pathAndValueExistsAndEmptyChecks() throws Exception { String query = "{me {name, friends}}"; - setup.response("{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}"); + setResponse("{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}"); - GraphQlTester.ResponseSpec spec = setup.graphQlTester().query(query).execute(); + GraphQlTester.ResponseSpec spec = this.graphQlTester.query(query).execute(); spec.path("me.name").pathExists().valueExists().valueIsNotEmpty(); spec.path("me.friends").valueIsEmpty(); spec.path("hero").pathDoesNotExist().valueDoesNotExist().valueIsEmpty(); - setup.verifyRequest((input) -> assertThat(input.getQuery()).contains(query)); - setup.shutdown(); + assertThat(getActualQuery()).contains(query); } - @ParameterizedTest - @MethodSource("argumentSource") - void matchesJson(GraphQlTesterSetup setup) throws Exception { + @Test + void matchesJson() throws Exception { String query = "{me {name}}"; - setup.response("{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}"); + setResponse("{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}"); - GraphQlTester.ResponseSpec spec = setup.graphQlTester().query(query).execute(); + GraphQlTester.ResponseSpec spec = this.graphQlTester.query(query).execute(); spec.path("").matchesJson("{\"me\": {\"name\":\"Luke Skywalker\",\"friends\":[]}}"); spec.path("me").matchesJson("{\"name\":\"Luke Skywalker\"}"); @@ -108,18 +94,16 @@ public class GraphQlTesterTests { assertThatThrownBy(() -> spec.path("me").matchesJsonStrictly("{\"friends\":[]}")) .as("Extended fields should fail in strict mode").hasMessageContaining("Unexpected: name"); - setup.verifyRequest((input) -> assertThat(input.getQuery()).contains(query)); - setup.shutdown(); + assertThat(getActualQuery()).contains(query); } - @ParameterizedTest - @MethodSource("argumentSource") - void entity(GraphQlTesterSetup setup) throws Exception { + @Test + void entity() throws Exception { String query = "{me {name}}"; - setup.response("{\"me\": {\"name\":\"Luke Skywalker\"}}"); + setResponse("{\"me\": {\"name\":\"Luke Skywalker\"}}"); - GraphQlTester.ResponseSpec spec = setup.graphQlTester().query(query).execute(); + GraphQlTester.ResponseSpec spec = this.graphQlTester.query(query).execute(); MovieCharacter luke = MovieCharacter.create("Luke Skywalker"); MovieCharacter han = MovieCharacter.create("Han Solo"); @@ -134,19 +118,17 @@ public class GraphQlTesterTests { spec.path("").entity(new ParameterizedTypeReference>() { }).isEqualTo(Collections.singletonMap("me", luke)); - setup.verifyRequest((input) -> assertThat(input.getQuery()).contains(query)); - setup.shutdown(); + assertThat(getActualQuery()).contains(query); } - @ParameterizedTest - @MethodSource("argumentSource") - void entityList(GraphQlTesterSetup setup) throws Exception { + @Test + void entityList() throws Exception { String query = "{me {name, friends}}"; - setup.response("{" + " \"me\":{" + " \"name\":\"Luke Skywalker\"," + setResponse("{" + " \"me\":{" + " \"name\":\"Luke Skywalker\"," + " \"friends\":[{\"name\":\"Han Solo\"}, {\"name\":\"Leia Organa\"}]" + " }" + "}"); - GraphQlTester.ResponseSpec spec = setup.graphQlTester().query(query).execute(); + GraphQlTester.ResponseSpec spec = this.graphQlTester.query(query).execute(); MovieCharacter han = MovieCharacter.create("Han Solo"); MovieCharacter leia = MovieCharacter.create("Leia Organa"); @@ -161,102 +143,89 @@ public class GraphQlTesterTests { spec.path("me.friends").entityList(new ParameterizedTypeReference() { }).containsExactly(han, leia); - setup.verifyRequest((input) -> assertThat(input.getQuery()).contains(query)); - setup.shutdown(); + assertThat(getActualQuery()).contains(query); } - @ParameterizedTest - @MethodSource("argumentSource") - void operationNameAndVariables(GraphQlTesterSetup setup) throws Exception { + @Test + void operationNameAndVariables() throws Exception { String query = "query HeroNameAndFriends($episode: Episode) {" + " hero(episode: $episode) {" + " name" + " }" + "}"; - setup.response("{\"hero\": {\"name\":\"R2-D2\"}}"); + setResponse("{\"hero\": {\"name\":\"R2-D2\"}}"); - GraphQlTester.ResponseSpec spec = setup.graphQlTester().query(query).operationName("HeroNameAndFriends") + GraphQlTester.ResponseSpec spec = this.graphQlTester.query(query).operationName("HeroNameAndFriends") .variable("episode", "JEDI").variables((map) -> map.put("foo", "bar")).execute(); spec.path("hero").entity(MovieCharacter.class).isEqualTo(MovieCharacter.create("R2-D2")); - setup.verifyRequest((input) -> { - assertThat(input.getQuery()).contains(query); - assertThat(input.getOperationName()).isEqualTo("HeroNameAndFriends"); - assertThat(input.getVariables()).hasSize(2); - assertThat(input.getVariables()).containsEntry("episode", "JEDI"); - assertThat(input.getVariables()).containsEntry("foo", "bar"); - }); - setup.shutdown(); + ExecutionInput input = this.inputCaptor.getValue(); + assertThat(input.getQuery()).contains(query); + assertThat(input.getOperationName()).isEqualTo("HeroNameAndFriends"); + assertThat(input.getVariables()).hasSize(2); + assertThat(input.getVariables()).containsEntry("episode", "JEDI"); + assertThat(input.getVariables()).containsEntry("foo", "bar"); } - @ParameterizedTest - @MethodSource("argumentSource") - void errorsCheckedOnExecuteAndVerify(GraphQlTesterSetup setup) throws Exception { + @Test + void errorsCheckedOnExecuteAndVerify() throws Exception { String query = "{me {name, friends}}"; - setup.response(GraphqlErrorBuilder.newError().message("Invalid query").build()); + setResponse(GraphqlErrorBuilder.newError().message("Invalid query").build()); - assertThatThrownBy(() -> setup.graphQlTester().query(query).executeAndVerify()) + assertThatThrownBy(() -> this.graphQlTester.query(query).executeAndVerify()) .hasMessageContaining("Response has 1 unexpected error(s)."); - setup.verifyRequest((input) -> assertThat(input.getQuery()).contains(query)); - setup.shutdown(); + assertThat(getActualQuery()).contains(query); } - @ParameterizedTest - @MethodSource("argumentSource") - void errorsCheckedOnTraverse(GraphQlTesterSetup setup) throws Exception { + @Test + void errorsCheckedOnTraverse() throws Exception { String query = "{me {name, friends}}"; - setup.response(GraphqlErrorBuilder.newError().message("Invalid query").build()); + setResponse(GraphqlErrorBuilder.newError().message("Invalid query").build()); - assertThatThrownBy(() -> setup.graphQlTester().query(query).execute().path("me")) + assertThatThrownBy(() -> this.graphQlTester.query(query).execute().path("me")) .hasMessageContaining("Response has 1 unexpected error(s)."); - setup.verifyRequest((input) -> assertThat(input.getQuery()).contains(query)); - setup.shutdown(); + assertThat(getActualQuery()).contains(query); } - @ParameterizedTest - @MethodSource("argumentSource") - void errorsPartiallyFiltered(GraphQlTesterSetup setup) throws Exception { + @Test + void errorsPartiallyFiltered() throws Exception { String query = "{me {name, friends}}"; - setup.response(GraphqlErrorBuilder.newError().message("some error").build(), + setResponse(GraphqlErrorBuilder.newError().message("some error").build(), GraphqlErrorBuilder.newError().message("some other error").build()); - assertThatThrownBy(() -> setup.graphQlTester().query(query).execute().errors() + assertThatThrownBy(() -> this.graphQlTester.query(query).execute().errors() .filter((error) -> error.getMessage().equals("some error")).verify()) .hasMessageContaining("Response has 1 unexpected error(s) of 2 total."); - setup.verifyRequest((input) -> assertThat(input.getQuery()).contains(query)); - setup.shutdown(); + assertThat(getActualQuery()).contains(query); } - @ParameterizedTest - @MethodSource("argumentSource") - void errorsFiltered(GraphQlTesterSetup setup) throws Exception { + @Test + void errorsFiltered() throws Exception { String query = "{me {name, friends}}"; - setup.response(GraphqlErrorBuilder.newError().message("some error").build(), + setResponse(GraphqlErrorBuilder.newError().message("some error").build(), GraphqlErrorBuilder.newError().message("some other error").build()); - setup.graphQlTester().query(query).execute().errors().filter((error) -> error.getMessage().startsWith("some ")) + this.graphQlTester.query(query).execute().errors().filter((error) -> error.getMessage().startsWith("some ")) .verify().path("me").pathDoesNotExist(); - setup.verifyRequest((input) -> assertThat(input.getQuery()).contains(query)); - setup.shutdown(); + assertThat(getActualQuery()).contains(query); } - @ParameterizedTest - @MethodSource("argumentSource") - void errorsConsumed(GraphQlTesterSetup setup) throws Exception { + @Test + void errorsConsumed() throws Exception { String query = "{me {name, friends}}"; - setup.response( + setResponse( GraphqlErrorBuilder.newError().message("Invalid query").location(new SourceLocation(1, 2)).build()); - setup.graphQlTester().query(query).execute().errors().satisfy((errors) -> { + this.graphQlTester.query(query).execute().errors().satisfy((errors) -> { assertThat(errors).hasSize(1); assertThat(errors.get(0).getMessage()).isEqualTo("Invalid query"); assertThat(errors.get(0).getLocations()).hasSize(1); @@ -264,134 +233,31 @@ public class GraphQlTesterTests { assertThat(errors.get(0).getLocations().get(0).getColumn()).isEqualTo(2); }).path("me").pathDoesNotExist(); - setup.verifyRequest((input) -> assertThat(input.getQuery()).contains(query)); - setup.shutdown(); + assertThat(getActualQuery()).contains(query); } - private interface GraphQlTesterSetup { - - GraphQlTester graphQlTester(); - - default void response(String data) throws Exception { - response(data, Collections.emptyList()); - } - - default void response(GraphQLError... errors) throws Exception { - response(null, Arrays.asList(errors)); - } - - void response(@Nullable String data, List errors) throws Exception; - - void verifyRequest(Consumer consumer) throws Exception; - - default void shutdown() throws Exception { - // no-op by default - } - + private void setResponse(String data) throws Exception { + setResponse(data, Collections.emptyList()); } - private static class MockWebServerSetup implements GraphQlTesterSetup { - - private final MockWebServer server; - - private final GraphQlTester graphQlTester; - - MockWebServerSetup() { - this.server = new MockWebServer(); - this.graphQlTester = GraphQlTester.create(initWebTestClient(this.server)); - } - - private static WebTestClient initWebTestClient(MockWebServer server) { - String baseUrl = server.url("/graphQL").toString(); - return WebTestClient.bindToServer().baseUrl(baseUrl).build(); - } - - @Override - public GraphQlTester graphQlTester() { - return this.graphQlTester; - } - - @Override - public void response(@Nullable String data, List errors) throws Exception { - StringBuilder sb = new StringBuilder("{"); - if (StringUtils.hasText(data)) { - sb.append("\"data\":").append(data); - } - if (!CollectionUtils.isEmpty(errors)) { - List> errorSpecs = errors.stream().map(GraphQLError::toSpecification) - .collect(Collectors.toList()); - - sb.append(StringUtils.hasText(data) ? ", " : "").append("\"errors\":") - .append(OBJECT_MAPPER.writeValueAsString(errorSpecs)); - } - sb.append("}"); - - MockResponse response = new MockResponse(); - response.setHeader("Content-Type", "application/json"); - response.setBody(sb.toString()); - - this.server.enqueue(response); - } - - @Override - public void verifyRequest(Consumer consumer) throws Exception { - assertThat(this.server.getRequestCount()).isEqualTo(1); - RecordedRequest request = this.server.takeRequest(); - assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo("application/json"); - - String content = request.getBody().readUtf8(); - Map map = new ObjectMapper().readValue(content, new TypeReference>() { - }); - WebInput webInput = new WebInput(request.getRequestUrl().uri(), new HttpHeaders(), map, null); - - consumer.accept(webInput); - } - - @Override - public void shutdown() throws Exception { - this.server.shutdown(); - } - + private void setResponse(GraphQLError... errors) throws Exception { + setResponse(null, Arrays.asList(errors)); } - private static class MockWebGraphQlHandlerSetup implements GraphQlTesterSetup { - - private final WebGraphQlHandler handler = mock(WebGraphQlHandler.class); - - private final ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(WebInput.class); - - private final GraphQlTester graphQlTester; - - MockWebGraphQlHandlerSetup() { - this.graphQlTester = GraphQlTester.create(this.handler); + private void setResponse(@Nullable String data, List errors) throws Exception { + ExecutionResultImpl.Builder builder = new ExecutionResultImpl.Builder(); + if (data != null) { + builder.data(OBJECT_MAPPER.readValue(data, new TypeReference>() {})); } - - @Override - public GraphQlTester graphQlTester() { - return this.graphQlTester; - } - - @Override - public void response(@Nullable String data, List errors) throws Exception { - ExecutionResultImpl.Builder builder = new ExecutionResultImpl.Builder(); - if (data != null) { - builder.data(OBJECT_MAPPER.readValue(data, new TypeReference>() { - })); - } - if (!CollectionUtils.isEmpty(errors)) { - builder.addErrors(errors); - } - ExecutionResult result = builder.build(); - WebOutput output = new WebOutput(mock(WebInput.class), result); - given(this.handler.handle(this.bodyCaptor.capture())).willReturn(Mono.just(output)); - } - - @Override - public void verifyRequest(Consumer consumer) { - WebInput webInput = this.bodyCaptor.getValue(); - consumer.accept(webInput); + if (!CollectionUtils.isEmpty(errors)) { + builder.addErrors(errors); } + ExecutionResult result = builder.build(); + given(this.service.execute(this.inputCaptor.capture())).willReturn(Mono.just(result)); + } + private String getActualQuery() { + return this.inputCaptor.getValue().getQuery(); } } diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterTests.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterTests.java new file mode 100644 index 00000000..8451b49e --- /dev/null +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/WebGraphQlTesterTests.java @@ -0,0 +1,216 @@ +/* + * Copyright 2002-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.test.tester; + +import java.util.Arrays; +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; +import java.util.stream.Collectors; +import java.util.stream.Stream; + +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; +import graphql.ExecutionResult; +import graphql.ExecutionResultImpl; +import graphql.GraphQLError; +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import okhttp3.mockwebserver.RecordedRequest; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.MethodSource; +import org.mockito.ArgumentCaptor; +import reactor.core.publisher.Mono; + +import org.springframework.graphql.web.WebGraphQlHandler; +import org.springframework.graphql.web.WebInput; +import org.springframework.graphql.web.WebOutput; +import org.springframework.http.HttpHeaders; +import org.springframework.lang.Nullable; +import org.springframework.test.web.reactive.server.WebTestClient; +import org.springframework.util.CollectionUtils; +import org.springframework.util.StringUtils; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link WebGraphQlTester} parameterized to: + *

    + *
  • Connect to {@link MockWebServer} and return a preset HTTP response. + *
  • Use mock {@link WebGraphQlHandler} to return a preset {@link ExecutionResult}. + *
+ * + *

+ * There is no actual handling via {@link graphql.GraphQL} in either scenario. The main + * focus is to verify {@link GraphQlTester} request preparation and response handling. + */ +public class WebGraphQlTesterTests { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + public static Stream argumentSource() { + return Stream.of(new MockWebServerSetup(), new MockWebGraphQlHandlerSetup()); + } + + @ParameterizedTest + @MethodSource("argumentSource") + void pathAndValueExistsAndEmptyChecks(GraphQlTesterSetup setup) throws Exception { + + String query = "{me {name, friends}}"; + setup.response("{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}"); + + GraphQlTester.ResponseSpec spec = setup.graphQlTester().query(query).execute(); + + spec.path("me.name").pathExists().valueExists().valueIsNotEmpty(); + spec.path("me.friends").valueIsEmpty(); + spec.path("hero").pathDoesNotExist().valueDoesNotExist().valueIsEmpty(); + + setup.verifyRequest((input) -> assertThat(input.getQuery()).contains(query)); + setup.shutdown(); + } + + + private interface GraphQlTesterSetup { + + GraphQlTester graphQlTester(); + + default void response(String data) throws Exception { + response(data, Collections.emptyList()); + } + + default void response(GraphQLError... errors) throws Exception { + response(null, Arrays.asList(errors)); + } + + void response(@Nullable String data, List errors) throws Exception; + + void verifyRequest(Consumer consumer) throws Exception; + + default void shutdown() throws Exception { + // no-op by default + } + + } + + private static class MockWebServerSetup implements GraphQlTesterSetup { + + private final MockWebServer server; + + private final GraphQlTester graphQlTester; + + MockWebServerSetup() { + this.server = new MockWebServer(); + this.graphQlTester = WebGraphQlTester.create(initWebTestClient(this.server)); + } + + private static WebTestClient initWebTestClient(MockWebServer server) { + String baseUrl = server.url("/graphQL").toString(); + return WebTestClient.bindToServer().baseUrl(baseUrl).build(); + } + + @Override + public GraphQlTester graphQlTester() { + return this.graphQlTester; + } + + @Override + public void response(@Nullable String data, List errors) throws Exception { + StringBuilder sb = new StringBuilder("{"); + if (StringUtils.hasText(data)) { + sb.append("\"data\":").append(data); + } + if (!CollectionUtils.isEmpty(errors)) { + List> errorSpecs = errors.stream().map(GraphQLError::toSpecification) + .collect(Collectors.toList()); + + sb.append(StringUtils.hasText(data) ? ", " : "").append("\"errors\":") + .append(OBJECT_MAPPER.writeValueAsString(errorSpecs)); + } + sb.append("}"); + + MockResponse response = new MockResponse(); + response.setHeader("Content-Type", "application/json"); + response.setBody(sb.toString()); + + this.server.enqueue(response); + } + + @Override + public void verifyRequest(Consumer consumer) throws Exception { + assertThat(this.server.getRequestCount()).isEqualTo(1); + RecordedRequest request = this.server.takeRequest(); + assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo("application/json"); + + String content = request.getBody().readUtf8(); + Map map = new ObjectMapper().readValue(content, new TypeReference>() { + }); + WebInput webInput = new WebInput(request.getRequestUrl().uri(), new HttpHeaders(), map, null); + + consumer.accept(webInput); + } + + @Override + public void shutdown() throws Exception { + this.server.shutdown(); + } + + } + + private static class MockWebGraphQlHandlerSetup implements GraphQlTesterSetup { + + private final WebGraphQlHandler handler = mock(WebGraphQlHandler.class); + + private final ArgumentCaptor bodyCaptor = ArgumentCaptor.forClass(WebInput.class); + + private final GraphQlTester graphQlTester; + + MockWebGraphQlHandlerSetup() { + this.graphQlTester = WebGraphQlTester.create(this.handler); + } + + @Override + public GraphQlTester graphQlTester() { + return this.graphQlTester; + } + + @Override + public void response(@Nullable String data, List errors) throws Exception { + ExecutionResultImpl.Builder builder = new ExecutionResultImpl.Builder(); + if (data != null) { + builder.data(OBJECT_MAPPER.readValue(data, new TypeReference>() { + })); + } + if (!CollectionUtils.isEmpty(errors)) { + builder.addErrors(errors); + } + ExecutionResult result = builder.build(); + WebOutput output = new WebOutput(mock(WebInput.class), result); + given(this.handler.handle(this.bodyCaptor.capture())).willReturn(Mono.just(output)); + } + + @Override + public void verifyRequest(Consumer consumer) { + WebInput webInput = this.bodyCaptor.getValue(); + consumer.accept(webInput); + } + + } + +}