Add fragments support in GraphQlTester
This commit adds support for GraphQL fragments with `GraphQlTester`.
Fragments allow to avoid repetition in GraphQL requests by reusing
field selection sets.
For example, the "releases" fragment can be reused in multiple queries
and make the overall document shorter:
```
query frameworkReleases {
project(slug: "spring-framework") {
name
...releases
}
}
query graphqlReleases {
project(slug: "spring-graphql") {
name
...releases
}
}
fragment releases on Project {
releases {
version
}
}
```
With this change, `GraphQlTester` accepts fragments as `String` or can
load them by their name using the configured `DocumentSource`, similarly
to the document support.
Closes gh-964
This commit is contained in:
@@ -476,6 +476,55 @@ You can then:
|
||||
<1> Load the document from "projectReleases.graphql"
|
||||
<2> Provide variable values.
|
||||
|
||||
|
||||
This approach also works for loading fragments for your queries.
|
||||
Fragments are reusable field selection sets that avoid repetition in a request document.
|
||||
For example, we can use a `...releases` fragment in multiple queries:
|
||||
|
||||
[source,graphql,indent=0,subs="verbatim,quotes"]
|
||||
.src/main/resources/graphql-documents/projectReleases.graphql
|
||||
----
|
||||
query frameworkReleases {
|
||||
project(slug: "spring-framework") {
|
||||
name
|
||||
...releases
|
||||
}
|
||||
}
|
||||
query graphqlReleases {
|
||||
project(slug: "spring-graphql") {
|
||||
name
|
||||
...releases
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
This fragment can be defined in a separate file for reuse:
|
||||
|
||||
[source,graphql,indent=0,subs="verbatim,quotes"]
|
||||
.src/main/resources/graphql-documents/releases.graphql
|
||||
----
|
||||
fragment releases on Project {
|
||||
releases {
|
||||
version
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
|
||||
You can then send this fragment along the query document:
|
||||
|
||||
[source,java,indent=0,subs="verbatim,quotes"]
|
||||
----
|
||||
Project project = graphQlClient.documentName("projectReleases") <1>
|
||||
.fragmentName("releases") <2>
|
||||
.retrieveSync()
|
||||
.toEntity(Project.class);
|
||||
----
|
||||
<1> Load the document from "projectReleases.graphql"
|
||||
<2> Load the fragment from "releases.graphql" and append it to the document
|
||||
|
||||
|
||||
|
||||
The "JS GraphQL" plugin for IntelliJ supports GraphQL query files with code completion.
|
||||
|
||||
You can use the `GraphQlClient` xref:client.adoc#client.graphqlclient.builder[Builder] to customize the
|
||||
|
||||
@@ -123,6 +123,8 @@ final class DefaultGraphQlTester implements GraphQlTester {
|
||||
@Nullable
|
||||
private String operationName;
|
||||
|
||||
List<String> fragments = new ArrayList<>();
|
||||
|
||||
private final Map<String, Object> variables = new LinkedHashMap<>();
|
||||
|
||||
private final Map<String, Object> extensions = new LinkedHashMap<>();
|
||||
@@ -138,6 +140,21 @@ final class DefaultGraphQlTester implements GraphQlTester {
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRequest fragment(String fragment) {
|
||||
Assert.hasText(fragment, "Fragment should not be empty");
|
||||
this.fragments.add(fragment);
|
||||
return this;
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRequest fragmentName(String fragmentName) {
|
||||
String fragment = DefaultGraphQlTester.this.documentSource.getDocument(fragmentName)
|
||||
.block(DefaultGraphQlTester.this.responseTimeout);
|
||||
Assert.hasText(fragment, "DocumentSource completed empty for fragment " + fragmentName);
|
||||
return this.fragment(fragment);
|
||||
}
|
||||
|
||||
@Override
|
||||
public DefaultRequest variable(String name, @Nullable Object value) {
|
||||
this.variables.put(name, value);
|
||||
@@ -176,7 +193,9 @@ final class DefaultGraphQlTester implements GraphQlTester {
|
||||
}
|
||||
|
||||
private GraphQlRequest request() {
|
||||
return new DefaultGraphQlRequest(this.document, this.operationName, this.variables, this.extensions);
|
||||
StringBuilder document = new StringBuilder(this.document);
|
||||
this.fragments.forEach(document::append);
|
||||
return new DefaultGraphQlRequest(document.toString(), this.operationName, this.variables, this.extensions);
|
||||
}
|
||||
|
||||
private DefaultResponse mapResponse(GraphQlResponse response, GraphQlRequest request) {
|
||||
|
||||
@@ -49,14 +49,14 @@ import org.springframework.lang.Nullable;
|
||||
* </ul>
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
* @since 1.0.0
|
||||
*/
|
||||
public interface GraphQlTester {
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* textual representation of an operation (or operations) to perform.
|
||||
* @param document the document for the request
|
||||
* @return spec for response assertions
|
||||
* @throws AssertionError if the response status is not 200 (OK)
|
||||
@@ -145,6 +145,28 @@ public interface GraphQlTester {
|
||||
*/
|
||||
T operationName(@Nullable String name);
|
||||
|
||||
/**
|
||||
* Append the given fragment section to the {@link #document(String) request document}.
|
||||
* A fragment describes a selection of fields to be included in the query when needed
|
||||
* and is defined with the {@code fragment} keyword.
|
||||
* @param fragment the fragment definition
|
||||
* @return this request spec
|
||||
* @since 1.3
|
||||
* @see <a href="http://spec.graphql.org/October2021/#sec-Language.Fragments">Fragments specification</a>
|
||||
*/
|
||||
T fragment(String fragment);
|
||||
|
||||
/**
|
||||
* Variant of {@link #fragment(String)} that uses the given key to resolve
|
||||
* the GraphQL fragment document from a file with the help of the configured
|
||||
* {@link Builder#documentSource(DocumentSource) DocumentSource}.
|
||||
* @param fragmentName the name of the fragment to append
|
||||
* @return this request spec
|
||||
* @throws IllegalArgumentException if the fragmentName cannot be resolved
|
||||
* @since 1.3
|
||||
*/
|
||||
T fragmentName(String fragmentName);
|
||||
|
||||
/**
|
||||
* Add a variable.
|
||||
* @param name the variable name
|
||||
|
||||
@@ -32,15 +32,35 @@ import org.springframework.graphql.ExecutionGraphQlService;
|
||||
import org.springframework.graphql.GraphQlRequest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for {@link GraphQlTester} with a mock {@link ExecutionGraphQlService}.
|
||||
*
|
||||
* @author Rossen Stoyanchev
|
||||
* @author Brian Clozel
|
||||
*/
|
||||
public class GraphQlTesterTests extends GraphQlTesterTestSupport {
|
||||
|
||||
@Test
|
||||
void missingDocumentByName() {
|
||||
String document = "{me {name, friends}}";
|
||||
getGraphQlService().setDataAsJson(document, "{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(() -> graphQlTester().documentName("unknown").execute())
|
||||
.withMessageContaining("Failed to find document, name='unknown', under location(s)=[class path resource [graphql-test/]]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void resolveDocumentByName() {
|
||||
String document = "{me {name, friends}}";
|
||||
getGraphQlService().setDataAsJson(document, "{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
|
||||
|
||||
GraphQlTester.Response response = graphQlTester().documentName("me").execute();
|
||||
response.path("me.name").hasValue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void hasValue() {
|
||||
|
||||
@@ -243,6 +263,58 @@ public class GraphQlTesterTests extends GraphQlTesterTestSupport {
|
||||
assertThat(request.getVariables()).containsEntry("keyOnly", null);
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentWithFragment() {
|
||||
String document = """
|
||||
query meQuery {
|
||||
me {name, ...friendsField}
|
||||
}
|
||||
""";
|
||||
String fragment =
|
||||
"""
|
||||
fragment friendsField on User {
|
||||
friends
|
||||
}
|
||||
""";
|
||||
getGraphQlService().setDataAsJson(document + fragment, "{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
|
||||
|
||||
GraphQlTester.Response response = graphQlTester().document(document).fragment(fragment).execute();
|
||||
response.path("me.name").hasValue();
|
||||
|
||||
assertThat(getActualRequestDocument()).contains(document);
|
||||
}
|
||||
|
||||
@Test
|
||||
void missingFragmentByName() {
|
||||
String document = "{me {name, friends}}";
|
||||
getGraphQlService().setDataAsJson(document, "{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
|
||||
|
||||
assertThatIllegalStateException().isThrownBy(() -> graphQlTester().document(document).fragmentName("unknown").execute())
|
||||
.withMessageContaining("Failed to find document, name='unknown', under location(s)=[class path resource [graphql-test/]]");
|
||||
}
|
||||
|
||||
@Test
|
||||
void documentWithFragmentName() {
|
||||
String document =
|
||||
"""
|
||||
query meQuery {
|
||||
me {name, ...friendsField}
|
||||
}
|
||||
""";
|
||||
String fragment =
|
||||
"""
|
||||
fragment friendsField on User {
|
||||
friends
|
||||
}
|
||||
""";
|
||||
getGraphQlService().setDataAsJson(document + fragment, "{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
|
||||
|
||||
GraphQlTester.Response response = graphQlTester().document(document).fragmentName("friends").execute();
|
||||
response.path("me.name").hasValue();
|
||||
|
||||
assertThat(getActualRequestDocument()).contains(document);
|
||||
}
|
||||
|
||||
@Test
|
||||
void variablesAsMap() {
|
||||
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
fragment friendsField on User {
|
||||
friends
|
||||
}
|
||||
@@ -0,0 +1 @@
|
||||
{me {name, friends}}
|
||||
Reference in New Issue
Block a user