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:
Brian Clozel
2024-06-03 18:27:19 +02:00
parent 43c32dd2e4
commit a9a8d0b745
6 changed files with 169 additions and 3 deletions

View File

@@ -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

View File

@@ -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) {

View File

@@ -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

View File

@@ -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() {

View File

@@ -0,0 +1,3 @@
fragment friendsField on User {
friends
}

View File

@@ -0,0 +1 @@
{me {name, friends}}