Skip to content

Commit

Permalink
Add fragments support in GraphQlTester
Browse files Browse the repository at this point in the history
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
  • Loading branch information
bclozel committed Jun 3, 2024
1 parent 43c32dd commit a9a8d0b
Show file tree
Hide file tree
Showing 6 changed files with 169 additions and 3 deletions.
49 changes: 49 additions & 0 deletions spring-graphql-docs/modules/ROOT/pages/client.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,8 @@ private final class DefaultRequest implements Request<DefaultRequest> {
@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<>();
Expand All @@ -138,6 +140,21 @@ public DefaultRequest operationName(@Nullable String name) {
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);
Expand Down Expand Up @@ -176,7 +193,9 @@ public Subscription executeSubscription() {
}

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) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -49,14 +49,14 @@
* </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)
Expand Down Expand Up @@ -145,6 +145,28 @@ interface Request<T extends Request<T>> {
*/
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
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -32,15 +32,35 @@
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() {

Expand Down Expand Up @@ -243,6 +263,58 @@ query HeroNameAndFriends($episode: Episode) {
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() {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
fragment friendsField on User {
friends
}
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
{me {name, friends}}

0 comments on commit a9a8d0b

Please sign in to comment.