Skip to content

Commit a9a8d0b

Browse files
committed
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
1 parent 43c32dd commit a9a8d0b

File tree

6 files changed

+169
-3
lines changed

6 files changed

+169
-3
lines changed

spring-graphql-docs/modules/ROOT/pages/client.adoc

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,55 @@ You can then:
476476
<1> Load the document from "projectReleases.graphql"
477477
<2> Provide variable values.
478478

479+
480+
This approach also works for loading fragments for your queries.
481+
Fragments are reusable field selection sets that avoid repetition in a request document.
482+
For example, we can use a `...releases` fragment in multiple queries:
483+
484+
[source,graphql,indent=0,subs="verbatim,quotes"]
485+
.src/main/resources/graphql-documents/projectReleases.graphql
486+
----
487+
query frameworkReleases {
488+
project(slug: "spring-framework") {
489+
name
490+
...releases
491+
}
492+
}
493+
query graphqlReleases {
494+
project(slug: "spring-graphql") {
495+
name
496+
...releases
497+
}
498+
}
499+
----
500+
501+
This fragment can be defined in a separate file for reuse:
502+
503+
[source,graphql,indent=0,subs="verbatim,quotes"]
504+
.src/main/resources/graphql-documents/releases.graphql
505+
----
506+
fragment releases on Project {
507+
releases {
508+
version
509+
}
510+
}
511+
----
512+
513+
514+
You can then send this fragment along the query document:
515+
516+
[source,java,indent=0,subs="verbatim,quotes"]
517+
----
518+
Project project = graphQlClient.documentName("projectReleases") <1>
519+
.fragmentName("releases") <2>
520+
.retrieveSync()
521+
.toEntity(Project.class);
522+
----
523+
<1> Load the document from "projectReleases.graphql"
524+
<2> Load the fragment from "releases.graphql" and append it to the document
525+
526+
527+
479528
The "JS GraphQL" plugin for IntelliJ supports GraphQL query files with code completion.
480529

481530
You can use the `GraphQlClient` xref:client.adoc#client.graphqlclient.builder[Builder] to customize the

spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,8 @@ private final class DefaultRequest implements Request<DefaultRequest> {
123123
@Nullable
124124
private String operationName;
125125

126+
List<String> fragments = new ArrayList<>();
127+
126128
private final Map<String, Object> variables = new LinkedHashMap<>();
127129

128130
private final Map<String, Object> extensions = new LinkedHashMap<>();
@@ -138,6 +140,21 @@ public DefaultRequest operationName(@Nullable String name) {
138140
return this;
139141
}
140142

143+
@Override
144+
public DefaultRequest fragment(String fragment) {
145+
Assert.hasText(fragment, "Fragment should not be empty");
146+
this.fragments.add(fragment);
147+
return this;
148+
}
149+
150+
@Override
151+
public DefaultRequest fragmentName(String fragmentName) {
152+
String fragment = DefaultGraphQlTester.this.documentSource.getDocument(fragmentName)
153+
.block(DefaultGraphQlTester.this.responseTimeout);
154+
Assert.hasText(fragment, "DocumentSource completed empty for fragment " + fragmentName);
155+
return this.fragment(fragment);
156+
}
157+
141158
@Override
142159
public DefaultRequest variable(String name, @Nullable Object value) {
143160
this.variables.put(name, value);
@@ -176,7 +193,9 @@ public Subscription executeSubscription() {
176193
}
177194

178195
private GraphQlRequest request() {
179-
return new DefaultGraphQlRequest(this.document, this.operationName, this.variables, this.extensions);
196+
StringBuilder document = new StringBuilder(this.document);
197+
this.fragments.forEach(document::append);
198+
return new DefaultGraphQlRequest(document.toString(), this.operationName, this.variables, this.extensions);
180199
}
181200

182201
private DefaultResponse mapResponse(GraphQlResponse response, GraphQlRequest request) {

spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@
4949
* </ul>
5050
*
5151
* @author Rossen Stoyanchev
52+
* @author Brian Clozel
5253
* @since 1.0.0
5354
*/
5455
public interface GraphQlTester {
5556

5657
/**
5758
* Start defining a GraphQL request with the given document, which is the
58-
* textual representation of an operation (or operations) to perform,
59-
* including selection sets and fragments.
59+
* textual representation of an operation (or operations) to perform.
6060
* @param document the document for the request
6161
* @return spec for response assertions
6262
* @throws AssertionError if the response status is not 200 (OK)
@@ -145,6 +145,28 @@ interface Request<T extends Request<T>> {
145145
*/
146146
T operationName(@Nullable String name);
147147

148+
/**
149+
* Append the given fragment section to the {@link #document(String) request document}.
150+
* A fragment describes a selection of fields to be included in the query when needed
151+
* and is defined with the {@code fragment} keyword.
152+
* @param fragment the fragment definition
153+
* @return this request spec
154+
* @since 1.3
155+
* @see <a href="http://spec.graphql.org/October2021/#sec-Language.Fragments">Fragments specification</a>
156+
*/
157+
T fragment(String fragment);
158+
159+
/**
160+
* Variant of {@link #fragment(String)} that uses the given key to resolve
161+
* the GraphQL fragment document from a file with the help of the configured
162+
* {@link Builder#documentSource(DocumentSource) DocumentSource}.
163+
* @param fragmentName the name of the fragment to append
164+
* @return this request spec
165+
* @throws IllegalArgumentException if the fragmentName cannot be resolved
166+
* @since 1.3
167+
*/
168+
T fragmentName(String fragmentName);
169+
148170
/**
149171
* Add a variable.
150172
* @param name the variable name

spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,35 @@
3232
import org.springframework.graphql.GraphQlRequest;
3333

3434
import static org.assertj.core.api.Assertions.assertThat;
35+
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
3536
import static org.assertj.core.api.Assertions.assertThatThrownBy;
3637

3738
/**
3839
* Tests for {@link GraphQlTester} with a mock {@link ExecutionGraphQlService}.
3940
*
4041
* @author Rossen Stoyanchev
42+
* @author Brian Clozel
4143
*/
4244
public class GraphQlTesterTests extends GraphQlTesterTestSupport {
4345

46+
@Test
47+
void missingDocumentByName() {
48+
String document = "{me {name, friends}}";
49+
getGraphQlService().setDataAsJson(document, "{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
50+
51+
assertThatIllegalStateException().isThrownBy(() -> graphQlTester().documentName("unknown").execute())
52+
.withMessageContaining("Failed to find document, name='unknown', under location(s)=[class path resource [graphql-test/]]");
53+
}
54+
55+
@Test
56+
void resolveDocumentByName() {
57+
String document = "{me {name, friends}}";
58+
getGraphQlService().setDataAsJson(document, "{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
59+
60+
GraphQlTester.Response response = graphQlTester().documentName("me").execute();
61+
response.path("me.name").hasValue();
62+
}
63+
4464
@Test
4565
void hasValue() {
4666

@@ -243,6 +263,58 @@ query HeroNameAndFriends($episode: Episode) {
243263
assertThat(request.getVariables()).containsEntry("keyOnly", null);
244264
}
245265

266+
@Test
267+
void documentWithFragment() {
268+
String document = """
269+
query meQuery {
270+
me {name, ...friendsField}
271+
}
272+
""";
273+
String fragment =
274+
"""
275+
fragment friendsField on User {
276+
friends
277+
}
278+
""";
279+
getGraphQlService().setDataAsJson(document + fragment, "{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
280+
281+
GraphQlTester.Response response = graphQlTester().document(document).fragment(fragment).execute();
282+
response.path("me.name").hasValue();
283+
284+
assertThat(getActualRequestDocument()).contains(document);
285+
}
286+
287+
@Test
288+
void missingFragmentByName() {
289+
String document = "{me {name, friends}}";
290+
getGraphQlService().setDataAsJson(document, "{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
291+
292+
assertThatIllegalStateException().isThrownBy(() -> graphQlTester().document(document).fragmentName("unknown").execute())
293+
.withMessageContaining("Failed to find document, name='unknown', under location(s)=[class path resource [graphql-test/]]");
294+
}
295+
296+
@Test
297+
void documentWithFragmentName() {
298+
String document =
299+
"""
300+
query meQuery {
301+
me {name, ...friendsField}
302+
}
303+
""";
304+
String fragment =
305+
"""
306+
fragment friendsField on User {
307+
friends
308+
}
309+
""";
310+
getGraphQlService().setDataAsJson(document + fragment, "{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
311+
312+
GraphQlTester.Response response = graphQlTester().document(document).fragmentName("friends").execute();
313+
response.path("me.name").hasValue();
314+
315+
assertThat(getActualRequestDocument()).contains(document);
316+
}
317+
246318
@Test
247319
void variablesAsMap() {
248320

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
fragment friendsField on User {
2+
friends
3+
}
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{me {name, friends}}

0 commit comments

Comments
 (0)