diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractDelegatingGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractDelegatingGraphQlTester.java
new file mode 100644
index 00000000..b1e0248f
--- /dev/null
+++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractDelegatingGraphQlTester.java
@@ -0,0 +1,52 @@
+/*
+ * Copyright 2002-2022 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;
+
+
+/**
+ * Base class for extensions of {@link GraphQlTester} that mainly assist with
+ * building the underlying transport, but otherwise delegate to the default
+ * {@link GraphQlTester} implementation for actual request execution.
+ *
+ *
Subclasses must implement {@link GraphQlTester#mutate()} to allow mutation
+ * of both {@code GraphQlTester} and {@code GraphQlTransport} configuration.
+ *
+ * @author Rossen Stoyanchev
+ * @since 1.0.0
+ * @see AbstractGraphQlTesterBuilder
+ */
+public abstract class AbstractDelegatingGraphQlTester implements GraphQlTester {
+
+ private final GraphQlTester delegate;
+
+
+ protected AbstractDelegatingGraphQlTester(GraphQlTester delegate) {
+ this.delegate = delegate;
+ }
+
+
+ @Override
+ public RequestSpec> document(String document) {
+ return this.delegate.document(document);
+ }
+
+ @Override
+ public RequestSpec> documentName(String documentName) {
+ return this.delegate.documentName(documentName);
+ }
+
+}
diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java
new file mode 100644
index 00000000..2bdb0e53
--- /dev/null
+++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/AbstractGraphQlTesterBuilder.java
@@ -0,0 +1,134 @@
+/*
+ * Copyright 2002-2022 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.time.Duration;
+import java.util.function.Consumer;
+import java.util.function.Predicate;
+
+import com.jayway.jsonpath.Configuration;
+import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
+import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
+import graphql.GraphQLError;
+
+import org.springframework.graphql.client.GraphQlTransport;
+import org.springframework.graphql.support.CachingDocumentSource;
+import org.springframework.graphql.support.DocumentSource;
+import org.springframework.graphql.support.ResourceDocumentSource;
+import org.springframework.lang.Nullable;
+import org.springframework.util.Assert;
+import org.springframework.util.ClassUtils;
+
+/**
+ * Abstract, base class for transport specific {@link GraphQlTester.Builder}
+ * implementations.
+ *
+ *
Subclasses must implement {@link #build()} and call
+ * {@link #buildGraphQlTester(GraphQlTransport)} to obtain a default, transport
+ * agnostic {@code GraphQlTester}. A transport specific extension can then wrap
+ * this default tester by extending {@link AbstractDelegatingGraphQlTester}.
+ *
+ * @author Rossen Stoyanchev
+ * @since 1.0.0
+ * @see AbstractDelegatingGraphQlTester
+ */
+public abstract class AbstractGraphQlTesterBuilder> implements GraphQlTester.Builder {
+
+ private static final boolean jackson2Present;
+
+ static {
+ ClassLoader classLoader = AbstractGraphQlTesterBuilder.class.getClassLoader();
+ jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
+ && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
+ }
+
+ private static final Duration DEFAULT_RESPONSE_DURATION = Duration.ofSeconds(5);
+
+
+ @Nullable
+ private Predicate errorFilter;
+
+ private DocumentSource documentSource = new CachingDocumentSource(new ResourceDocumentSource());
+
+ private Duration responseTimeout = DEFAULT_RESPONSE_DURATION;
+
+
+ @Override
+ public B errorFilter(Predicate predicate) {
+ this.errorFilter = (this.errorFilter != null ? errorFilter.and(predicate) : predicate);
+ return self();
+ }
+
+ @Override
+ public B documentSource(DocumentSource documentSource) {
+ this.documentSource = documentSource;
+ return self();
+ }
+
+ @Override
+ public B responseTimeout(Duration timeout) {
+ Assert.notNull(timeout, "'timeout' is required");
+ this.responseTimeout = timeout;
+ return self();
+ }
+
+ @SuppressWarnings("unchecked")
+ private T self() {
+ return (T) this;
+ }
+
+ /**
+ * Subclasses call this from {@link #build()} to provide the transport and get
+ * the default {@code GraphQlTester} to delegate to for request execution.
+ */
+ protected GraphQlTester buildGraphQlTester(GraphQlTransport transport) {
+ Assert.notNull(transport, "GraphQlTransport is required");
+ return new DefaultGraphQlTester(
+ transport, this.errorFilter, initJsonPathConfig(), this.documentSource, this.responseTimeout,
+ getBuilderInitializer());
+ }
+
+ private Configuration initJsonPathConfig() {
+ // Allow configuring JSONPath with codecs from transport subclasses
+ return (jackson2Present ? Jackson2Configuration.create() : Configuration.builder().build());
+ }
+
+ /**
+ * Subclasses call this from {@link #build()} to obtain a {@code Consumer} to
+ * initialize new builder instances with, based on "this" builder.
+ */
+ protected Consumer> getBuilderInitializer() {
+ return builder -> {
+ if (this.errorFilter != null) {
+ builder.errorFilter(this.errorFilter);
+ }
+ builder.documentSource(this.documentSource);
+ builder.responseTimeout(this.responseTimeout);
+ };
+ }
+
+
+ private static class Jackson2Configuration {
+
+ static Configuration create() {
+ return Configuration.builder()
+ .jsonProvider(new JacksonJsonProvider())
+ .mappingProvider(new JacksonMappingProvider())
+ .build();
+ }
+ }
+
+}
diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlServiceTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlServiceTester.java
new file mode 100644
index 00000000..151afcce
--- /dev/null
+++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlServiceTester.java
@@ -0,0 +1,87 @@
+/*
+ * Copyright 2002-2022 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.function.Consumer;
+
+import org.springframework.graphql.GraphQlService;
+import org.springframework.util.Assert;
+
+
+/**
+ * Default {@link GraphQlServiceTester} that uses a {@link GraphQlService} for
+ * request execution.
+ *
+ * @author Rossen Stoyanchev
+ * @since 1.0.0
+ */
+final class DefaultGraphQlServiceTester extends AbstractDelegatingGraphQlTester implements GraphQlServiceTester {
+
+ private final GraphQlServiceTransport transport;
+
+ private final Consumer> builderInitializer;
+
+
+ DefaultGraphQlServiceTester(GraphQlTester tester, GraphQlServiceTransport transport,
+ Consumer> builderInitializer) {
+
+ super(tester);
+
+ Assert.notNull(transport, "GraphQlServiceTransport is required");
+ Assert.notNull(builderInitializer, "`builderInitializer` is required");
+
+ this.transport = transport;
+ this.builderInitializer = builderInitializer;
+ }
+
+
+ @Override
+ public Builder> mutate() {
+ Builder> builder = new Builder<>(this.transport);
+ this.builderInitializer.accept(builder);
+ return builder;
+ }
+
+
+ /**
+ * Default {@link GraphQlServiceTester.Builder} implementation.
+ */
+ static class Builder> extends AbstractGraphQlTesterBuilder
+ implements GraphQlServiceTester.Builder {
+
+ private final GraphQlService service;
+
+ Builder(GraphQlService service) {
+ Assert.notNull(service, "GraphQlService is required");
+ this.service = service;
+ }
+
+ Builder(GraphQlServiceTransport transport) {
+ this.service = transport.getGraphQlService();
+ }
+
+ @Override
+ public GraphQlServiceTester build() {
+ GraphQlServiceTransport transport = new GraphQlServiceTransport(this.service);
+ GraphQlTester tester = super.buildGraphQlTester(transport);
+ return new DefaultGraphQlServiceTester(tester, transport, getBuilderInitializer());
+ }
+
+ }
+
+}
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 9ee6f63c..f21d9ad9 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,23 +16,29 @@
package org.springframework.graphql.test.tester;
+import java.time.Duration;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
+import java.util.LinkedHashMap;
import java.util.List;
-import java.util.Locale;
+import java.util.Map;
import java.util.function.Consumer;
-import java.util.function.Function;
import java.util.function.Predicate;
import java.util.stream.Collectors;
+import com.jayway.jsonpath.Configuration;
import com.jayway.jsonpath.DocumentContext;
import com.jayway.jsonpath.JsonPath;
import com.jayway.jsonpath.PathNotFoundException;
import com.jayway.jsonpath.TypeRef;
+import graphql.ExecutionResult;
import graphql.GraphQLError;
import org.springframework.core.ParameterizedTypeReference;
+import org.springframework.graphql.GraphQlRequest;
+import org.springframework.graphql.client.GraphQlTransport;
+import org.springframework.graphql.support.DocumentSource;
import org.springframework.lang.Nullable;
import org.springframework.test.util.AssertionErrors;
import org.springframework.test.util.JsonExpectationsHelper;
@@ -42,98 +48,158 @@ import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;
/**
- * Default implementation of {@link GraphQlTester}.
+ * Default {@link GraphQlTester} implementation with the logic to initialize
+ * requests and handle responses. It is transport agnostic and depends on a
+ * {@link GraphQlTransport} to execute requests with.
+ *
+ *
This class is final but works with any transport.
*
* @author Rossen Stoyanchev
*/
-class DefaultGraphQlTester implements GraphQlTester {
+final class DefaultGraphQlTester implements GraphQlTester {
- private final RequestStrategy requestStrategy;
+ private final GraphQlTransport transport;
- private final Function queryNameResolver;
+ @Nullable
+ private final Predicate errorFilter;
+
+ private final Configuration jsonPathConfig;
+
+ private final DocumentSource documentSource;
+
+ private final Duration responseTimeout;
+
+ private final Consumer> builderInitializer;
- DefaultGraphQlTester(RequestStrategy requestStrategy, Function queryNameResolver) {
- Assert.notNull(requestStrategy, "RequestStrategy is required.");
- Assert.notNull(queryNameResolver, "'queryNameResolver' is required.");
- this.requestStrategy = requestStrategy;
- this.queryNameResolver = queryNameResolver;
+ /**
+ * Package private constructor for use from {@link AbstractGraphQlTesterBuilder}.
+ */
+ DefaultGraphQlTester(
+ GraphQlTransport transport, @Nullable Predicate errorFilter,
+ Configuration jsonPathConfig, DocumentSource documentSource, Duration timeout,
+ Consumer> builderInitializer) {
+
+ this.transport = transport;
+ this.errorFilter = errorFilter;
+ this.jsonPathConfig = jsonPathConfig;
+ this.documentSource = documentSource;
+ this.responseTimeout = timeout;
+ this.builderInitializer = builderInitializer;
}
@Override
- public RequestSpec> query(String query) {
- return new DefaultRequestSpec(this.requestStrategy, query);
+ public RequestSpec> document(String document) {
+ return new DefaultRequestSpec(document);
}
@Override
- public RequestSpec> queryName(String queryName) {
- return query(this.queryNameResolver.apply(queryName));
+ public RequestSpec> documentName(String documentName) {
+ String document = this.documentSource.getDocument(documentName).block(this.responseTimeout);
+ Assert.notNull(document, "Expected document content or an error");
+ return document(document);
+ }
+
+ @Override
+ public Builder mutate() {
+ Builder builder = new Builder(this.transport);
+ this.builderInitializer.accept(builder);
+ return builder;
}
/**
- * Factory for {@link GraphQlTester.ResponseSpec}, for use from
- * {@link RequestStrategy} implementations.
- *
- * @param documentContext the parsed response content
- * @param errorFilter a globally defined filter for expected errors (to be ignored)
- * @param assertDecorator decorator to apply around assertions, e.g. to add extra
+ * Default {@link GraphQlTester.Builder} with a given transport.
*/
- static GraphQlTester.ResponseSpec createResponseSpec(
- DocumentContext documentContext, @Nullable Predicate errorFilter,
- Consumer assertDecorator) {
+ static final class Builder extends AbstractGraphQlTesterBuilder {
+
+ private final GraphQlTransport transport;
+
+ Builder(GraphQlTransport transport) {
+ this.transport = transport;
+ }
+
+ @Override
+ public GraphQlTester build() {
+ return super.buildGraphQlTester(this.transport);
+ }
- return new DefaultResponseSpec(documentContext, errorFilter, assertDecorator);
}
/**
- * {@link RequestSpec} that collects the query, operationName, and variables.
+ * {@link RequestSpec} that gathers the document, operationName, and variables.
*/
- private static final class DefaultRequestSpec
- extends GraphQlTesterRequestSpecSupport implements RequestSpec {
+ private final class DefaultRequestSpec implements RequestSpec {
- private final RequestStrategy requestStrategy;
+ private final String document;
- private DefaultRequestSpec(RequestStrategy requestStrategy, String query) {
- super(query);
- Assert.notNull(requestStrategy, "RequestStrategy is required");
- this.requestStrategy = requestStrategy;
+ @Nullable
+ private String operationName;
+
+ private final Map variables = new LinkedHashMap<>();
+
+ private DefaultRequestSpec(String document) {
+ Assert.notNull(document, "`document` is required");
+ this.document = document;
}
@Override
public DefaultRequestSpec operationName(@Nullable String name) {
- setOperationName(name);
+ this.operationName = name;
return this;
}
@Override
public DefaultRequestSpec variable(String name, @Nullable Object value) {
- addVariable(name, value);
- return this;
- }
-
- @Override
- public DefaultRequestSpec locale(Locale locale) {
- setLocale(locale);
+ this.variables.put(name, value);
return this;
}
+ @SuppressWarnings("ConstantConditions")
@Override
public ResponseSpec execute() {
- return this.requestStrategy.execute(createRequestInput());
+ GraphQlRequest request = createRequest();
+ return transport.execute(request)
+ .map(result -> createResponseSpec(result, assertDecorator(request)))
+ .block(responseTimeout);
}
@Override
public void executeAndVerify() {
- verify(execute());
+ execute().path("$.errors").valueIsEmpty();
}
@Override
public SubscriptionSpec executeSubscription() {
- return this.requestStrategy.executeSubscription(createRequestInput());
+ GraphQlRequest request = createRequest();
+ return () -> transport.executeSubscription(request)
+ .map(result -> createResponseSpec(result, assertDecorator(request)));
}
+
+ private GraphQlRequest createRequest() {
+ return new GraphQlRequest(this.document, this.operationName, this.variables);
+ }
+
+ private GraphQlTester.ResponseSpec createResponseSpec(
+ ExecutionResult result, Consumer assertDecorator) {
+
+ DocumentContext jsonDocument = JsonPath.parse(result.toSpecification(), jsonPathConfig);
+ return new DefaultResponseSpec(jsonDocument, errorFilter, assertDecorator);
+ }
+
+ private Consumer assertDecorator(GraphQlRequest request) {
+ return (assertion) -> {
+ try {
+ assertion.run();
+ }
+ catch (AssertionError ex) {
+ throw new AssertionError(ex.getMessage() + "\nRequest: " + request, ex);
+ }
+ };
+ }
+
}
diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTesterBuilder.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTesterBuilder.java
deleted file mode 100644
index 89be89d2..00000000
--- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTesterBuilder.java
+++ /dev/null
@@ -1,71 +0,0 @@
-/*
- * 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.time.Duration;
-import java.util.function.Predicate;
-
-import com.jayway.jsonpath.Configuration;
-import graphql.GraphQLError;
-
-import org.springframework.graphql.GraphQlService;
-import org.springframework.util.Assert;
-
-/**
- * Default implementation of a {@link GraphQlTester.Builder}.
- *
- * @author Rossen Stoyanchev
- * @since 1.0.0
- */
-class DefaultGraphQlTesterBuilder
- extends GraphQlTesterBuilderSupport implements GraphQlTester.Builder {
-
- private final GraphQlService service;
-
-
- DefaultGraphQlTesterBuilder(GraphQlService service) {
- Assert.notNull(service, "GraphQlService is required.");
- this.service = service;
- }
-
-
- @Override
- public DefaultGraphQlTesterBuilder errorFilter(Predicate predicate) {
- addErrorFilter(predicate);
- return this;
- }
-
- @Override
- public DefaultGraphQlTesterBuilder jsonPathConfig(Configuration config) {
- setJsonPathConfig(config);
- return this;
- }
-
- @Override
- public DefaultGraphQlTesterBuilder responseTimeout(Duration timeout) {
- setResponseTimeout(timeout);
- return this;
- }
-
- @Override
- public GraphQlTester build() {
- RequestStrategy strategy = new GraphQlServiceRequestStrategy(
- this.service, getErrorFilter(), initJsonPathConfig(), initResponseTimeout());
-
- return new DefaultGraphQlTester(strategy, getQueryNameResolver());
- }
-
-}
diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java
new file mode 100644
index 00000000..46e6d708
--- /dev/null
+++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultHttpGraphQlTester.java
@@ -0,0 +1,119 @@
+/*
+ * Copyright 2002-2022 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.util.function.Consumer;
+
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.codec.CodecConfigurer;
+import org.springframework.test.web.reactive.server.WebTestClient;
+import org.springframework.web.util.DefaultUriBuilderFactory;
+import org.springframework.web.util.UriBuilderFactory;
+import org.springframework.web.util.UriComponentsBuilder;
+
+/**
+ * Default {@link HttpGraphQlTester} that builds and uses a {@link WebTestClient}
+ * for request execution.
+ *
+ * @author Rossen Stoyanchev
+ * @since 1.0.0
+ */
+final class DefaultHttpGraphQlTester extends AbstractDelegatingGraphQlTester implements HttpGraphQlTester {
+
+ private final WebTestClient webTestClient;
+
+ private final Consumer> builderInitializer;
+
+
+ DefaultHttpGraphQlTester(GraphQlTester graphQlTester, WebTestClient webTestClient,
+ Consumer> builderInitializer) {
+
+ super(graphQlTester);
+ this.webTestClient = webTestClient;
+ this.builderInitializer = builderInitializer;
+ }
+
+
+ @Override
+ public Builder mutate() {
+ Builder builder = new Builder(this.webTestClient.mutate());
+ this.builderInitializer.accept(builder);
+ return builder;
+ }
+
+
+ /**
+ * Default {@link HttpGraphQlTester.Builder} implementation.
+ */
+ static final class Builder extends AbstractGraphQlTesterBuilder
+ implements HttpGraphQlTester.Builder {
+
+ private final WebTestClient.Builder webTestClientBuilder;
+
+ Builder(WebTestClient.Builder clientBuilder) {
+ this.webTestClientBuilder = clientBuilder;
+ }
+
+ @Override
+ public Builder url(String url) {
+ this.webTestClientBuilder.baseUrl(url);
+ return this;
+ }
+
+ @Override
+ public Builder url(URI url) {
+ UriBuilderFactory factory = new DefaultUriBuilderFactory(UriComponentsBuilder.fromUri(url));
+ this.webTestClientBuilder.uriBuilderFactory(factory);
+ return this;
+ }
+
+ @Override
+ public Builder header(String name, String... values) {
+ this.webTestClientBuilder.defaultHeader(name, values);
+ return this;
+ }
+
+ @Override
+ public Builder headers(Consumer headersConsumer) {
+ this.webTestClientBuilder.defaultHeaders(headersConsumer);
+ return this;
+ }
+
+ @Override
+ public Builder codecConfigurer(Consumer codecConsumer) {
+ this.webTestClientBuilder.codecs(codecConsumer::accept);
+ return this;
+ }
+
+ @Override
+ public Builder webTestClient(Consumer configurer) {
+ configurer.accept(this.webTestClientBuilder);
+ return this;
+ }
+
+ @Override
+ public HttpGraphQlTester build() {
+ WebTestClient client = this.webTestClientBuilder.build();
+ GraphQlTester tester = super.buildGraphQlTester(new WebTestClientTransport(client));
+ return new DefaultHttpGraphQlTester(tester, client, getBuilderInitializer());
+ }
+
+ }
+
+}
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
index 9140d409..86a52656 100644
--- 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
@@ -1,5 +1,5 @@
/*
- * Copyright 2002-2021 the original author or authors.
+ * Copyright 2002-2022 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.
@@ -16,221 +16,108 @@
package org.springframework.graphql.test.tester;
+
import java.net.URI;
-import java.util.Locale;
+import java.util.Arrays;
import java.util.function.Consumer;
-import java.util.function.Function;
-import reactor.core.publisher.Flux;
-
-import org.springframework.graphql.RequestInput;
-import org.springframework.graphql.web.WebInput;
+import org.springframework.graphql.web.WebGraphQlHandler;
import org.springframework.http.HttpHeaders;
-import org.springframework.lang.Nullable;
+import org.springframework.http.codec.CodecConfigurer;
import org.springframework.util.Assert;
-import org.springframework.util.CollectionUtils;
-import org.springframework.util.ObjectUtils;
+import org.springframework.web.util.DefaultUriBuilderFactory;
/**
- * Default implementation of {@link WebGraphQlTester}.
+ * Default {@link WebGraphQlTester} that uses {@link WebGraphQlHandler} for
+ * request execution.
*
* @author Rossen Stoyanchev
+ * @since 1.0.0
*/
-class DefaultWebGraphQlTester implements WebGraphQlTester {
+final class DefaultWebGraphQlTester extends AbstractDelegatingGraphQlTester implements WebGraphQlTester {
- private final WebRequestStrategy requestStrategy;
+ private final WebGraphQlHandlerTransport transport;
- @Nullable
- private final HttpHeaders defaultHeaders;
-
- private final Function queryNameResolver;
+ private final Consumer> builderInitializer;
- DefaultWebGraphQlTester(
- WebRequestStrategy requestStrategy, @Nullable HttpHeaders defaultHeaders,
- Function queryNameResolver) {
+ DefaultWebGraphQlTester(GraphQlTester tester, WebGraphQlHandlerTransport transport,
+ Consumer> builderInitializer) {
- Assert.notNull(requestStrategy, "WebRequestStrategy is required.");
- this.requestStrategy = requestStrategy;
- this.defaultHeaders = defaultHeaders;
- this.queryNameResolver = queryNameResolver;
+ super(tester);
+ this.transport = transport;
+ this.builderInitializer = builderInitializer;
}
@Override
- public WebRequestSpec query(String query) {
- return new DefaultWebRequestSpec(this.requestStrategy, this.defaultHeaders, query);
- }
-
- @Override
- public WebRequestSpec queryName(String queryName) {
- return query(this.queryNameResolver.apply(queryName));
+ public Builder> mutate() {
+ Builder> builder = new Builder<>(this.transport.getGraphQlHandler());
+ builder.url(this.transport.getUrl());
+ builder.headers(headers -> headers.putAll(this.transport.getHeaders()));
+ this.builderInitializer.accept(builder);
+ return builder;
}
/**
- * Factory for {@link WebGraphQlTester.ResponseSpec}, for use from
- * {@link WebRequestStrategy} implementations.
+ * Base builder implementation for all Web transport extensions.
*/
- static WebResponseSpec createResponseSpec(
- ResponseSpec responseSpec, @Nullable HttpHeaders responseHeaders) {
+ static class Builder> extends AbstractGraphQlTesterBuilder
+ implements WebGraphQlTester.Builder {
- return new DefaultWebResponseSpec(responseSpec, responseHeaders);
- }
-
- /**
- * Factory for {@link WebGraphQlTester.SubscriptionSpec}, for use from
- * {@link WebRequestStrategy} implementations.
- */
- static WebSubscriptionSpec createSubscriptionSpec(
- SubscriptionSpec subscriptionSpec, @Nullable HttpHeaders responseHeaders) {
-
- return new DefaultWebSubscriptionSpec(subscriptionSpec, responseHeaders);
- }
-
-
- /**
- * {@link WebRequestSpec} that also collects HTTP request headers, in
- * addition to the query, operationName, and variables.
- */
- private static final class DefaultWebRequestSpec
- extends GraphQlTesterRequestSpecSupport implements WebRequestSpec {
-
- private static final URI DEFAULT_URL = URI.create("");
-
- private final WebRequestStrategy requestStrategy;
+ private URI url = URI.create("");
private final HttpHeaders headers = new HttpHeaders();
- private DefaultWebRequestSpec(
- WebRequestStrategy requestStrategy, @Nullable HttpHeaders defaultHeaders, String query) {
+ private final WebGraphQlHandler handler;
- super(query);
- Assert.notNull(requestStrategy, "WebRequestStrategy is required");
- this.requestStrategy = requestStrategy;
- if (!CollectionUtils.isEmpty(defaultHeaders)) {
- this.headers.putAll(defaultHeaders);
- }
+ Builder(WebGraphQlHandler handler) {
+ Assert.notNull(handler, "WebGraphQlHandler is required");
+ this.handler = handler;
}
@Override
- public WebRequestSpec operationName(@Nullable String name) {
- setOperationName(name);
- return this;
+ public B url(String url) {
+ return url(new DefaultUriBuilderFactory().uriString(url).build());
}
@Override
- public WebRequestSpec variable(String name, @Nullable Object value) {
- addVariable(name, value);
- return this;
+ public B url(URI url) {
+ this.url = url;
+ return self();
}
@Override
- public WebRequestSpec locale(Locale locale) {
- setLocale(locale);
- return this;
+ public B header(String name, String... values) {
+ this.headers.put(name, Arrays.asList(values));
+ return self();
}
@Override
- public WebRequestSpec httpHeader(String headerName, String... headerValues) {
- for (String headerValue : headerValues) {
- this.headers.add(headerName, headerValue);
- }
- return this;
- }
-
- @Override
- public WebRequestSpec httpHeaders(Consumer headersConsumer) {
+ public B headers(Consumer headersConsumer) {
headersConsumer.accept(this.headers);
- return this;
+ return self();
}
@Override
- public WebResponseSpec execute() {
- return this.requestStrategy.execute(createWebInput());
+ public B codecConfigurer(Consumer codecConsumer) {
+ // Ignore, no serialization needs at this level
+ return self();
+ }
+
+ @SuppressWarnings("unchecked")
+ protected T self() {
+ return (T) this;
}
@Override
- public void executeAndVerify() {
- verify(execute());
+ public WebGraphQlTester build() {
+ WebGraphQlHandlerTransport transport = new WebGraphQlHandlerTransport(this.url, this.headers, this.handler);
+ GraphQlTester tester = super.buildGraphQlTester(transport);
+ return new DefaultWebGraphQlTester(tester, transport, getBuilderInitializer());
}
- @Override
- public WebSubscriptionSpec executeSubscription() {
- return this.requestStrategy.executeSubscription(createWebInput());
- }
-
- private WebInput createWebInput() {
- RequestInput input = createRequestInput();
- return new WebInput(DEFAULT_URL, this.headers, input.toMap(), input.getLocale(),
- (input.getId() != null) ? input.getId() : ObjectUtils.getIdentityHexString(input));
- }
- }
-
-
- /**
- * {@link WebResponseSpec} that exposes response headers and delegates
- * all other methods to the given {@link GraphQlTester.ResponseSpec}.
- */
- private static final class DefaultWebResponseSpec implements WebResponseSpec {
-
- private final ResponseSpec responseSpec;
-
- private final HttpHeaders responseHeaders;
-
- public DefaultWebResponseSpec(ResponseSpec responseSpec, @Nullable HttpHeaders responseHeaders) {
- this.responseSpec = responseSpec;
- this.responseHeaders = (responseHeaders != null ? responseHeaders : new HttpHeaders());
- }
-
- @Override
- public ResponseSpec httpHeadersSatisfy(Consumer consumer) {
- consumer.accept(this.responseHeaders);
- return this;
- }
-
- @Override
- public PathSpec path(String path) {
- return this.responseSpec.path(path);
- }
-
- @Override
- public ErrorSpec errors() {
- return this.responseSpec.errors();
- }
- }
-
-
- /**
- * {@link WebSubscriptionSpec} that exposes response headers and delegates
- * all other methods to the given {@link GraphQlTester.SubscriptionSpec}.
- */
- private static final class DefaultWebSubscriptionSpec implements WebSubscriptionSpec {
-
- private final SubscriptionSpec delegate;
-
- private final HttpHeaders headers;
-
- private DefaultWebSubscriptionSpec(SubscriptionSpec delegate, @Nullable HttpHeaders headers) {
- this.delegate = delegate;
- this.headers = (headers != null ? headers : new HttpHeaders());
- }
-
- @Override
- public SubscriptionSpec httpHeadersSatisfy(Consumer consumer) {
- consumer.accept(this.headers);
- return this;
- }
-
- @Override
- public Flux toFlux(String path, Class entityType) {
- return this.delegate.toFlux(path, entityType);
- }
-
- @Override
- public Flux toFlux() {
- return this.delegate.toFlux();
- }
}
}
diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTesterBuilder.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTesterBuilder.java
deleted file mode 100644
index 0fc76e65..00000000
--- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebGraphQlTesterBuilder.java
+++ /dev/null
@@ -1,120 +0,0 @@
-/*
- * 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.time.Duration;
-import java.util.function.Consumer;
-import java.util.function.Predicate;
-
-import com.jayway.jsonpath.Configuration;
-import graphql.GraphQLError;
-
-import org.springframework.graphql.web.WebGraphQlHandler;
-import org.springframework.http.HttpHeaders;
-import org.springframework.lang.Nullable;
-import org.springframework.test.web.reactive.server.WebTestClient;
-import org.springframework.util.Assert;
-
-/**
- * Default implementation of a {@link WebGraphQlTester.Builder}.
- *
- * @author Rossen Stoyanchev
- * @since 1.0.0
- */
-final class DefaultWebGraphQlTesterBuilder
- extends GraphQlTesterBuilderSupport implements WebGraphQlTester.Builder {
-
- @Nullable
- private final WebTestClient client;
-
- @Nullable
- private final WebGraphQlHandler handler;
-
- @Nullable
- private HttpHeaders headers;
-
-
- DefaultWebGraphQlTesterBuilder(WebTestClient client) {
- Assert.notNull(client, "WebTestClient is required.");
- this.client = client;
- this.handler = null;
- }
-
- DefaultWebGraphQlTesterBuilder(WebGraphQlHandler handler) {
- Assert.notNull(handler, "WebGraphQlHandler is required.");
- this.handler = handler;
- this.client = null;
- }
-
-
- @Override
- public WebGraphQlTester.Builder errorFilter(Predicate predicate) {
- addErrorFilter(predicate);
- return this;
- }
-
- @Override
- public DefaultWebGraphQlTesterBuilder jsonPathConfig(Configuration config) {
- setJsonPathConfig(config);
- return this;
- }
-
- @Override
- public DefaultWebGraphQlTesterBuilder responseTimeout(Duration timeout) {
- setResponseTimeout(timeout);
- return this;
- }
-
- @Override
- public DefaultWebGraphQlTesterBuilder defaultHttpHeader(String headerName, String... headerValues) {
- this.headers = (this.headers != null ? this.headers : new HttpHeaders());
- for (String headerValue : headerValues) {
- this.headers.add(headerName, headerValue);
- }
- return this;
- }
-
- @Override
- public WebGraphQlTester.Builder defaultHttpHeaders(Consumer headersConsumer) {
- this.headers = (this.headers != null ? this.headers : new HttpHeaders());
- headersConsumer.accept(this.headers);
- return this;
- }
-
- @Override
- public WebGraphQlTester build() {
- return new DefaultWebGraphQlTester(initRequestStrategy(), this.headers, getQueryNameResolver());
- }
-
- private WebRequestStrategy initRequestStrategy() {
- if (this.client != null) {
- WebTestClient clientToUse = this.client;
- if (getResponseTimeout() != null) {
- clientToUse = this.client.mutate().responseTimeout(getResponseTimeout()).build();
- }
- return new WebTestClientRequestStrategy(
- clientToUse, getErrorFilter(), initJsonPathConfig(), getResponseTimeout());
- }
-
- if (this.handler != null) {
- return new WebGraphQlHandlerRequestStrategy(
- this.handler, getErrorFilter(), initJsonPathConfig(), initResponseTimeout());
- }
-
- throw new IllegalStateException("Neither client nor handler");
- }
-
-}
diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java
new file mode 100644
index 00000000..00735f1c
--- /dev/null
+++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultWebSocketGraphQlTester.java
@@ -0,0 +1,170 @@
+/*
+ * Copyright 2002-2022 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.util.function.Consumer;
+
+import graphql.ExecutionResult;
+import reactor.core.publisher.Flux;
+import reactor.core.publisher.Mono;
+
+import org.springframework.graphql.GraphQlRequest;
+import org.springframework.graphql.client.GraphQlClient;
+import org.springframework.graphql.client.GraphQlTransport;
+import org.springframework.graphql.client.WebSocketGraphQlClient;
+import org.springframework.http.HttpHeaders;
+import org.springframework.http.codec.CodecConfigurer;
+import org.springframework.util.Assert;
+import org.springframework.web.reactive.socket.client.WebSocketClient;
+
+/**
+ * Default {@link WebSocketGraphQlTester} that builds and uses a
+ * {@link WebSocketGraphQlClient} for request execution.
+ *
+ * @author Rossen Stoyanchev
+ * @since 1.0.0
+ */
+final class DefaultWebSocketGraphQlTester extends AbstractDelegatingGraphQlTester implements WebSocketGraphQlTester {
+
+ private final WebSocketGraphQlClient webSocketGraphQlClient;
+
+ private final Consumer> builderInitializer;
+
+
+ DefaultWebSocketGraphQlTester(
+ GraphQlTester graphQlTester, WebSocketGraphQlClient webSocketGraphQlClient,
+ Consumer> builderInitializer) {
+
+ super(graphQlTester);
+ this.webSocketGraphQlClient = webSocketGraphQlClient;
+ this.builderInitializer = builderInitializer;
+ }
+
+
+ @Override
+ public Mono start() {
+ return this.webSocketGraphQlClient.start();
+ }
+
+ @Override
+ public Mono stop() {
+ return this.webSocketGraphQlClient.stop();
+ }
+
+ @Override
+ public Builder mutate() {
+ Builder builder = new Builder(this.webSocketGraphQlClient);
+ this.builderInitializer.accept(builder);
+ return builder;
+ }
+
+
+ /**
+ * Default {@link WebSocketGraphQlTester.Builder} implementation.
+ */
+ static final class Builder extends AbstractGraphQlTesterBuilder implements WebSocketGraphQlTester.Builder {
+
+ private final WebSocketGraphQlClient.Builder> graphQlClientBuilder;
+
+ /**
+ * Constructor to start via {@link WebSocketGraphQlTester#builder(URI, WebSocketClient)}.
+ */
+ Builder(URI url, WebSocketClient webSocketClient) {
+ Assert.notNull(webSocketClient, "WebSocketClient is required");
+ this.graphQlClientBuilder = WebSocketGraphQlClient.builder(url, webSocketClient);
+ }
+
+ /**
+ * Constructor to mutate.
+ * @param client the underlying client with the current state
+ */
+ Builder(WebSocketGraphQlClient client) {
+ Assert.notNull(client, "WebSocketGraphQlClient is required");
+ this.graphQlClientBuilder = client.mutate();
+ }
+
+
+ @Override
+ public Builder url(String url) {
+ this.graphQlClientBuilder.url(url);
+ return this;
+ }
+
+ @Override
+ public Builder url(URI url) {
+ this.graphQlClientBuilder.url(url);
+ return this;
+ }
+
+ @Override
+ public Builder header(String name, String... values) {
+ this.graphQlClientBuilder.header(name, values);
+ return this;
+ }
+
+ @Override
+ public Builder headers(Consumer headersConsumer) {
+ this.graphQlClientBuilder.headers(headersConsumer);
+ return this;
+ }
+
+ @Override
+ public Builder codecConfigurer(Consumer codecsConsumer) {
+ this.graphQlClientBuilder.codecConfigurer(codecsConsumer);
+ return this;
+ }
+
+ @Override
+ public WebSocketGraphQlTester build() {
+ WebSocketGraphQlClient client = this.graphQlClientBuilder.build();
+ GraphQlTester graphQlTester = super.buildGraphQlTester(asTransport(client));
+ return new DefaultWebSocketGraphQlTester(graphQlTester, client, getBuilderInitializer());
+ }
+
+ /**
+ * GraphQlTransport implementations are private, but we can create the
+ * GraphQlClient for it and adapt it.
+ */
+ private static GraphQlTransport asTransport(GraphQlClient client) {
+ return new GraphQlTransport() {
+
+ @Override
+ public Mono execute(GraphQlRequest request) {
+ return client
+ .document(request.getDocument())
+ .operationName(request.getOperationName())
+ .variables(request.getVariables())
+ .execute()
+ .map(GraphQlClient.ResponseSpec::andReturn);
+ }
+
+ @Override
+ public Flux executeSubscription(GraphQlRequest request) {
+ return client
+ .document(request.getDocument())
+ .operationName(request.getOperationName())
+ .variables(request.getVariables())
+ .executeSubscription().map(GraphQlClient.ResponseSpec::andReturn);
+ }
+ };
+ }
+
+ }
+
+}
diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlServiceTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlServiceTester.java
new file mode 100644
index 00000000..d95c21c4
--- /dev/null
+++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlServiceTester.java
@@ -0,0 +1,63 @@
+/*
+ * Copyright 2002-2022 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.GraphQlService;
+
+/**
+ * {@link GraphQlTester} that executes requests through a {@link GraphQlService}
+ * Use it for server-side tests, without a client.
+ *
+ * @author Rossen Stoyanchev
+ * @since 1.0.0
+ */
+public interface GraphQlServiceTester extends GraphQlTester {
+
+
+ @Override
+ Builder> mutate();
+
+
+ /**
+ * Create a {@link GraphQlServiceTester} instance.
+ */
+ static GraphQlServiceTester create(GraphQlService service) {
+ return builder(service).build();
+ }
+
+ /**
+ * Return a builder for {@link GraphQlServiceTester}.
+ */
+ static GraphQlServiceTester.Builder> builder(GraphQlService service) {
+ return new DefaultGraphQlServiceTester.Builder<>(service);
+ }
+
+
+ /**
+ * Default {@link GraphQlServiceTester.Builder} implementation.
+ */
+ interface Builder> extends GraphQlTester.Builder {
+
+ /**
+ * Build a {@link GraphQlServiceTester} instance.
+ */
+ @Override
+ GraphQlServiceTester build();
+
+ }
+
+}
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 080e0355..72aa413e 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
@@ -18,26 +18,34 @@ package org.springframework.graphql.test.tester;
import java.time.Duration;
import java.util.List;
-import java.util.Locale;
import java.util.function.Consumer;
import java.util.function.Predicate;
-import com.jayway.jsonpath.Configuration;
import graphql.GraphQLError;
import reactor.core.publisher.Flux;
import org.springframework.core.ParameterizedTypeReference;
-import org.springframework.graphql.GraphQlService;
+import org.springframework.graphql.client.GraphQlTransport;
+import org.springframework.graphql.support.DocumentSource;
+import org.springframework.graphql.support.ResourceDocumentSource;
import org.springframework.lang.Nullable;
/**
- * Contract for testing GraphQL requests.
+ * Define a workflow to test GraphQL requests that is independent of the
+ * underlying transport.
*
- *
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}.
+ *
To test using a client that connects to a server, with or without a live
+ * server, see {@code GraphQlTester} extensions:
+ *
+ *
{@link HttpGraphQlTester}
+ *
{@link WebSocketGraphQlTester}
+ *
+ *
+ *
To test on the server side, without a client, see the following:
+ *
+ *
{@link GraphQlServiceTester}
+ *
{@link WebGraphQlTester}
+ *
*
* @author Rossen Stoyanchev
* @since 1.0.0
@@ -45,49 +53,49 @@ import org.springframework.lang.Nullable;
public interface GraphQlTester {
/**
- * Prepare to perform a GraphQL request with the given operation which may
- * be a query, mutation, or a subscription.
- * @param query the operation to be performed
+ * Start defining a GraphQL request with the given document, which is the
+ * textual representation of an operation (or operations) to perform,
+ * including selection sets and fragments.
+ * @param document the document for the request
* @return spec for response assertions
* @throws AssertionError if the response status is not 200 (OK)
*/
- RequestSpec> query(String query);
+ RequestSpec> document(String document);
/**
- * Refer to a query by name where the given name is to look for a file with
- * the same name and extension {@code ".graphql"} or {@code ".gql"} under
- * classpath location {@code "graphql/"}.
+ * Variant of {@link #document(String)} that uses the given key to resolve
+ * the GraphQL document from a file, or in another way with the help of the
+ * {@link DocumentSource} that the client is configured with.
* @return spec for response assertions
- * @throws IllegalArgumentException if the queryName cannot be resolved
+ * @throws IllegalArgumentException if the documentName cannot be resolved
* @throws AssertionError if the response status is not 200 (OK)
*/
- RequestSpec> queryName(String queryName);
+ RequestSpec> documentName(String documentName);
+
+ /**
+ * Create a builder initialized from the configuration of "this" tester.
+ * Use it to build a new, independently configured instance.
+ */
+ Builder> mutate();
/**
- * Create a {@code GraphQlTester} that performs GraphQL requests through the
- * given {@link GraphQlService}.
- * @param service the service to execute requests with
- * @return the created {@code GraphQlTester}
+ * Create a builder with a custom {@code GraphQlTransport}.
+ *
For most cases, use a transport specific extension such as
+ * {@link HttpGraphQlTester} or {@link WebSocketGraphQlTester}. This method
+ * is for use with a custom {@code GraphQlTransport}.
+ * @param transport the transport to execute requests with
+ * @return the builder for further initialization
*/
- static GraphQlTester create(GraphQlService service) {
- return builder(service).build();
- }
-
- /**
- * Return a builder with options to initialize a {@code GraphQlTester}.
- * @param service the service to execute requests with
- * @return the builder to use
- */
- static Builder> builder(GraphQlService service) {
- return new DefaultGraphQlTesterBuilder(service);
+ static GraphQlTester.Builder> builder(GraphQlTransport transport) {
+ return new DefaultGraphQlTester.Builder(transport);
}
/**
* A builder to create a {@link GraphQlTester} instance.
*/
- interface Builder> {
+ interface Builder> {
/**
* Configure a global {@link ErrorSpec#filter(Predicate) filter} that
@@ -95,26 +103,21 @@ public interface GraphQlTester {
* @param predicate the error filter to add
* @return the same builder instance
*/
- T errorFilter(Predicate predicate);
+ B errorFilter(Predicate predicate);
/**
- * Provide JSONPath configuration settings, including a
- * {@link com.jayway.jsonpath.spi.json.JsonProvider} as well as a
- * {@link com.jayway.jsonpath.spi.mapper.MappingProvider} that are used
- * to serialize and deserialize GraphQL JSON content.
- *
By default the configuration is to use Jackson JSON if it is
- * present on the classpath.
- * @param config the JSONPath configuration to use
- * @return the same builder instance
+ * Configure a {@link DocumentSource} for use with
+ * {@link #documentName(String)} for resolving a document by name.
+ *
By default, {@link ResourceDocumentSource} is used.
*/
- T jsonPathConfig(Configuration config);
+ B documentSource(DocumentSource contentLoader);
/**
* Max amount of time to wait for a GraphQL response.
*
By default this is set to 5 seconds.
* @param timeout the response timeout value
*/
- T responseTimeout(Duration timeout);
+ B responseTimeout(Duration timeout);
/**
* Build the {@code GraphQlTester}.
@@ -174,13 +177,6 @@ public interface GraphQlTester {
*/
T variable(String name, @Nullable Object value);
- /**
- * Set the locale to associate with the request.
- * @param locale the locale to use
- * @return this request spec
- */
- T locale(Locale locale);
-
}
/**
diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterBuilderSupport.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterBuilderSupport.java
deleted file mode 100644
index 8f1e2a0f..00000000
--- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterBuilderSupport.java
+++ /dev/null
@@ -1,157 +0,0 @@
-/*
- * 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.io.ByteArrayOutputStream;
-import java.io.IOException;
-import java.nio.charset.StandardCharsets;
-import java.time.Duration;
-import java.util.Arrays;
-import java.util.function.Function;
-import java.util.function.Predicate;
-
-import com.jayway.jsonpath.Configuration;
-import com.jayway.jsonpath.spi.json.JacksonJsonProvider;
-import com.jayway.jsonpath.spi.mapper.JacksonMappingProvider;
-import graphql.GraphQLError;
-
-import org.springframework.core.io.ClassPathResource;
-import org.springframework.core.io.Resource;
-import org.springframework.lang.Nullable;
-import org.springframework.util.Assert;
-import org.springframework.util.ClassUtils;
-import org.springframework.util.FileCopyUtils;
-
-/**
- * Base class support for implementations of
- * {@link GraphQlTester.Builder} and {@link WebGraphQlTester.Builder}.
- *
- * @author Rossen Stoyanchev
- * @since 1.0.0
- */
-class GraphQlTesterBuilderSupport {
-
- private static final boolean jackson2Present;
-
- static {
- ClassLoader classLoader = GraphQlTesterBuilderSupport.class.getClassLoader();
- jackson2Present = ClassUtils.isPresent("com.fasterxml.jackson.databind.ObjectMapper", classLoader)
- && ClassUtils.isPresent("com.fasterxml.jackson.core.JsonGenerator", classLoader);
- }
-
- private static final Duration DEFAULT_RESPONSE_DURATION = Duration.ofSeconds(5);
-
-
- @Nullable
- private Predicate errorFilter;
-
- @Nullable
- private Configuration jsonPathConfig;
-
- @Nullable
- private Duration responseTimeout;
-
- private final Function queryNameResolver = new QueryNameResolver();
-
-
- protected void addErrorFilter(Predicate predicate) {
- this.errorFilter = (this.errorFilter != null ? errorFilter.and(predicate) : predicate);
- }
-
- @Nullable
- protected Predicate getErrorFilter() {
- return errorFilter;
- }
-
- protected void setJsonPathConfig(Configuration config) {
- this.jsonPathConfig = config;
- }
-
- protected void setResponseTimeout(Duration timeout) {
- Assert.notNull(timeout, "'timeout' is required");
- this.responseTimeout = timeout;
- }
-
- @Nullable
- protected Duration getResponseTimeout() {
- return this.responseTimeout;
- }
-
- protected Function getQueryNameResolver() {
- return this.queryNameResolver;
- }
-
- protected Configuration initJsonPathConfig() {
- if (this.jsonPathConfig != null) {
- return this.jsonPathConfig;
- }
- else if (jackson2Present) {
- return Jackson2Configuration.create();
- }
- else {
- return Configuration.builder().build();
- }
- }
-
- protected Duration initResponseTimeout() {
- return (this.responseTimeout != null ? this.responseTimeout : DEFAULT_RESPONSE_DURATION);
- }
-
-
- private static class QueryNameResolver implements Function {
-
- private static final ClassPathResource LOCATION = new ClassPathResource("graphql/");
-
- private static final String[] EXTENSIONS = new String[] {".graphql", ".gql"};
-
- @Override
- public String apply(String queryName) {
- Resource queryResource = getQueryResource(queryName);
- ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
- try {
- FileCopyUtils.copy(queryResource.getInputStream(), outputStream);
- }
- catch (IOException ex) {
- throw new IllegalArgumentException("Failed to read query from: " + LOCATION.getPath());
- }
- return new String(outputStream.toByteArray(), StandardCharsets.UTF_8);
- }
-
- private Resource getQueryResource(String queryName) {
- for (String extension : EXTENSIONS) {
- Resource resource = LOCATION.createRelative(queryName + extension);
- if (resource.exists()) {
- return resource;
- }
- }
- throw new IllegalArgumentException(
- "Could not find file '" + queryName + "' with extensions " + Arrays.toString(EXTENSIONS) +
- " under " + LOCATION.getDescription());
- }
- }
-
-
- private static class Jackson2Configuration {
-
- static Configuration create() {
- return Configuration.builder()
- .jsonProvider(new JacksonJsonProvider())
- .mappingProvider(new JacksonMappingProvider())
- .build();
- }
- }
-
-}
diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterRequestSpecSupport.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterRequestSpecSupport.java
deleted file mode 100644
index b380817b..00000000
--- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTesterRequestSpecSupport.java
+++ /dev/null
@@ -1,76 +0,0 @@
-/*
- * 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.LinkedHashMap;
-import java.util.Locale;
-import java.util.Map;
-
-import org.springframework.graphql.RequestInput;
-import org.springframework.lang.Nullable;
-import org.springframework.util.AlternativeJdkIdGenerator;
-import org.springframework.util.Assert;
-import org.springframework.util.IdGenerator;
-
-/**
- * Base class support for implementations of
- * {@link GraphQlTester.RequestSpec} and {@link WebGraphQlTester.RequestSpec}.
- *
- * @author Rossen Stoyanchev
- * @since 1.0.0
- */
-class GraphQlTesterRequestSpecSupport {
-
- private static final IdGenerator idGenerator = new AlternativeJdkIdGenerator();
-
- private final String query;
-
- @Nullable
- private String operationName;
-
- private final Map variables = new LinkedHashMap<>();
-
- @Nullable
- private Locale locale;
-
-
- protected GraphQlTesterRequestSpecSupport(String query) {
- Assert.notNull(query, "`query` is required");
- this.query = query;
- }
-
-
- protected void setOperationName(@Nullable String name) {
- this.operationName = name;
- }
-
- protected void addVariable(String name, @Nullable Object value) {
- this.variables.put(name, value);
- }
-
- protected void setLocale(Locale locale) {
- this.locale = locale;
- }
-
- protected void verify(GraphQlTester.ResponseSpec responseSpec) {
- responseSpec.path("$.errors").valueIsEmpty();
- }
-
- protected RequestInput createRequestInput() {
- return new RequestInput(this.query, this.operationName, this.variables, idGenerator.generateId().toString(), this.locale);
- }
-
-}
diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/HttpGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/HttpGraphQlTester.java
new file mode 100644
index 00000000..8a795daf
--- /dev/null
+++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/HttpGraphQlTester.java
@@ -0,0 +1,77 @@
+/*
+ * Copyright 2002-2022 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.function.Consumer;
+
+import org.springframework.test.web.reactive.server.WebTestClient;
+
+/**
+ * GraphQL over HTTP tester that uses {@link WebTestClient} and supports tests
+ * with or without a running server, depending on how {@code WebTestClient} is
+ * configured.
+ *
+ * @author Rossen Stoyanchev
+ * @since 1.0.0
+ */
+public interface HttpGraphQlTester extends WebGraphQlTester {
+
+
+ @Override
+ Builder> mutate();
+
+
+ /**
+ * Create an {@link HttpGraphQlTester} that uses the given {@link WebTestClient}.
+ */
+ static HttpGraphQlTester create(WebTestClient webTestClient) {
+ return builder(webTestClient.mutate()).build();
+ }
+
+ /**
+ * Return a builder to initialize an {@link HttpGraphQlTester} by creating
+ * the underlying {@link WebTestClient} through the given builder.
+ */
+ static HttpGraphQlTester.Builder> builder(WebTestClient.Builder webTestClientBuilder) {
+ return new DefaultHttpGraphQlTester.Builder(webTestClientBuilder);
+ }
+
+
+ /**
+ * Builder for the GraphQL over HTTP tester.
+ */
+ interface Builder> extends WebGraphQlTester.Builder {
+
+ /**
+ * Customize the {@code WebTestClient} to use.
+ *
Note that some properties of {@code WebTestClient.Builder} like the
+ * base URL, headers, and codecs can be customized through this builder.
+ * @see #url(String)
+ * @see #header(String, String...)
+ * @see #codecConfigurer(Consumer)
+ */
+ B webTestClient(Consumer webClient);
+
+ /**
+ * Build the {@code HttpGraphQlTester} instance.
+ */
+ @Override
+ HttpGraphQlTester build();
+
+ }
+
+}
diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/TestExecutionResult.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/TestExecutionResult.java
deleted file mode 100644
index b9949166..00000000
--- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/TestExecutionResult.java
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * 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.ArrayList;
-import java.util.Collections;
-import java.util.LinkedHashMap;
-import java.util.List;
-import java.util.Map;
-
-import graphql.ExecutionResult;
-import graphql.ExecutionResultImpl;
-import graphql.GraphQLError;
-
-import org.springframework.lang.Nullable;
-
-/**
- * {@link GraphQLError} with setters, for internal use to use to deserialize
- * from a response.
- *
- * @author Rossen Stoyanchev
- * @since 1.0.0
- */
-final class TestExecutionResult implements ExecutionResult {
-
- @Nullable
- private Object data;
-
- private List errors = Collections.emptyList();
-
- private Map