Decouple GraphQlTester from Web details
Closes gh-65
This commit is contained in:
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
|
||||
|
||||
@@ -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")));
|
||||
}
|
||||
|
||||
|
||||
@@ -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<byte[]> 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<TestExecutionResult> 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<GraphQLError> errors = result.getErrors();
|
||||
Consumer<Runnable> 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<Runnable> 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<Runnable> assertDecorator) {
|
||||
protected DefaultResponseSpec(DocumentContext documentContext, Consumer<Runnable> 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<ExecutionResult> publisher;
|
||||
|
||||
@@ -722,7 +678,7 @@ class DefaultGraphQlTester implements GraphQlTester {
|
||||
|
||||
private final Consumer<Runnable> assertDecorator;
|
||||
|
||||
<T> DefaultSubscriptionSpec(Publisher<ExecutionResult> publisher, Configuration jsonPathConfig,
|
||||
protected <T> DefaultSubscriptionSpec(Publisher<ExecutionResult> publisher, Configuration jsonPathConfig,
|
||||
Consumer<Runnable> decorator) {
|
||||
|
||||
this.publisher = publisher;
|
||||
|
||||
@@ -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<byte[]> 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<TestExecutionResult> 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;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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.
|
||||
*
|
||||
*
|
||||
* <p>
|
||||
* GraphQL requests to Spring MVC without an HTTP server: <pre class="code">
|
||||
* @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);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* GraphQL requests to Spring WebFlux without an HTTP server: <pre class="code">
|
||||
* @SpringBootTest
|
||||
* @AutoConfigureWebTestClient
|
||||
* public class MyTests {
|
||||
*
|
||||
* private GraphQlTester graphQlTester;
|
||||
*
|
||||
* @BeforeEach
|
||||
* public void setUp(@Autowired WebTestClient client) {
|
||||
* this.graphQlTester = GraphQlTester.create(client);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* GraphQL requests to a running server: <pre class="code">
|
||||
* @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
* public class MyTests {
|
||||
*
|
||||
* private GraphQlTester graphQlTester;
|
||||
*
|
||||
* @BeforeEach
|
||||
* public void setUp(@Autowired WebTestClient client) {
|
||||
* this.graphQlTester = GraphQlTester.create(client);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* GraphQL requests to any {@link WebGraphQlHandler}: <pre class="code">
|
||||
* @SpringBootTest
|
||||
* public class MyTests {
|
||||
*
|
||||
* private GraphQlTester graphQlTester;
|
||||
*
|
||||
* @BeforeEach
|
||||
* public void setUp(@Autowired WebGraphQLHandler handler) {
|
||||
* this.graphQlTester = GraphQlTester.create(handler);
|
||||
* }
|
||||
* </pre>
|
||||
* <p>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.
|
||||
*/
|
||||
|
||||
@@ -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.
|
||||
*
|
||||
* <p>
|
||||
* GraphQL requests to Spring MVC without an HTTP server: <pre class="code">
|
||||
* @SpringBootTest
|
||||
* @AutoConfigureMockMvc
|
||||
* @AutoConfigureGraphQlTester
|
||||
* public class MyTests {
|
||||
*
|
||||
* @Autowired
|
||||
* private WebGraphQlTester graphQlTester;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* GraphQL requests to Spring WebFlux without an HTTP server: <pre class="code">
|
||||
* @SpringBootTest
|
||||
* @AutoConfigureWebTestClient
|
||||
* @AutoConfigureGraphQlTester
|
||||
* public class MyTests {
|
||||
*
|
||||
* @Autowired
|
||||
* private WebGraphQlTester graphQlTester;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* GraphQL requests to a running server: <pre class="code">
|
||||
* @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
* @AutoConfigureWebTestClient
|
||||
* @AutoConfigureGraphQlTester
|
||||
* public class MyTests {
|
||||
*
|
||||
* @Autowired
|
||||
* private WebGraphQlTester graphQlTester;
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* <p>
|
||||
* GraphQL requests handled directly through a {@link WebGraphQlHandler}: <pre class="code">
|
||||
* @SpringBootTest
|
||||
* public class MyTests {
|
||||
*
|
||||
* private WebGraphQlTester graphQlTester;
|
||||
*
|
||||
* @BeforeEach
|
||||
* public void setUp(@Autowired WebGraphQLHandler handler) {
|
||||
* this.graphQlTester = GraphQlTester.create(handler);
|
||||
* }
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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:
|
||||
* <ul>
|
||||
* <li>Connect to {@link MockWebServer} and return a preset HTTP response.
|
||||
* <li>Use mock {@link WebGraphQlHandler} to return a preset {@link ExecutionResult}.
|
||||
* </ul>
|
||||
* Tests for {@link GraphQlTester}.
|
||||
*
|
||||
* <p>
|
||||
* 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<GraphQlTesterSetup> 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<ExecutionInput> 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<Map<String, MovieCharacter>>() {
|
||||
}).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<MovieCharacter>() {
|
||||
}).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<GraphQLError> errors) throws Exception;
|
||||
|
||||
void verifyRequest(Consumer<WebInput> 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<GraphQLError> errors) throws Exception {
|
||||
StringBuilder sb = new StringBuilder("{");
|
||||
if (StringUtils.hasText(data)) {
|
||||
sb.append("\"data\":").append(data);
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(errors)) {
|
||||
List<Map<String, Object>> 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<WebInput> 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<String, Object> map = new ObjectMapper().readValue(content, new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
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<WebInput> bodyCaptor = ArgumentCaptor.forClass(WebInput.class);
|
||||
|
||||
private final GraphQlTester graphQlTester;
|
||||
|
||||
MockWebGraphQlHandlerSetup() {
|
||||
this.graphQlTester = GraphQlTester.create(this.handler);
|
||||
private void setResponse(@Nullable String data, List<GraphQLError> errors) throws Exception {
|
||||
ExecutionResultImpl.Builder builder = new ExecutionResultImpl.Builder();
|
||||
if (data != null) {
|
||||
builder.data(OBJECT_MAPPER.readValue(data, new TypeReference<Map<String, Object>>() {}));
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQlTester graphQlTester() {
|
||||
return this.graphQlTester;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void response(@Nullable String data, List<GraphQLError> errors) throws Exception {
|
||||
ExecutionResultImpl.Builder builder = new ExecutionResultImpl.Builder();
|
||||
if (data != null) {
|
||||
builder.data(OBJECT_MAPPER.readValue(data, new TypeReference<Map<String, Object>>() {
|
||||
}));
|
||||
}
|
||||
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<WebInput> 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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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:
|
||||
* <ul>
|
||||
* <li>Connect to {@link MockWebServer} and return a preset HTTP response.
|
||||
* <li>Use mock {@link WebGraphQlHandler} to return a preset {@link ExecutionResult}.
|
||||
* </ul>
|
||||
*
|
||||
* <p>
|
||||
* 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<GraphQlTesterSetup> 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<GraphQLError> errors) throws Exception;
|
||||
|
||||
void verifyRequest(Consumer<WebInput> 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<GraphQLError> errors) throws Exception {
|
||||
StringBuilder sb = new StringBuilder("{");
|
||||
if (StringUtils.hasText(data)) {
|
||||
sb.append("\"data\":").append(data);
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(errors)) {
|
||||
List<Map<String, Object>> 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<WebInput> 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<String, Object> map = new ObjectMapper().readValue(content, new TypeReference<Map<String, Object>>() {
|
||||
});
|
||||
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<WebInput> 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<GraphQLError> errors) throws Exception {
|
||||
ExecutionResultImpl.Builder builder = new ExecutionResultImpl.Builder();
|
||||
if (data != null) {
|
||||
builder.data(OBJECT_MAPPER.readValue(data, new TypeReference<Map<String, Object>>() {
|
||||
}));
|
||||
}
|
||||
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<WebInput> consumer) {
|
||||
WebInput webInput = this.bodyCaptor.getValue();
|
||||
consumer.accept(webInput);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user