Skip to content

Commit 21f4520

Browse files
committed
Support nested paths in GraphQlTester
Closes gh-457
1 parent 2e7b010 commit 21f4520

File tree

4 files changed

+118
-27
lines changed

4 files changed

+118
-27
lines changed

spring-graphql-docs/src/docs/asciidoc/includes/testing.adoc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -359,6 +359,26 @@ See <<testing.errors>> for more details on error handling.
359359

360360

361361

362+
[[testing.requests.nestedPaths]]
363+
=== Nested Paths
364+
365+
By default, paths are relative to the "data" section of the GraphQL response. You can also
366+
nest down to a path, and inspect multiple paths relative to it as follows:
367+
368+
[source,java,indent=0,subs="verbatim,quotes"]
369+
----
370+
graphQlTester.document(document)
371+
.execute()
372+
.path("project", project -> project // <1>
373+
.path("name").entity(String.class).isEqualTo("spring-framework")
374+
.path("releases[*].version").entityList(String.class).hasSizeGreaterThan(1));
375+
----
376+
377+
<1> Use a callback to inspect paths relative to "project".
378+
379+
380+
381+
362382

363383
[[testing.subscriptions]]
364384
== Subscriptions

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

Lines changed: 60 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -145,7 +145,7 @@ public DefaultRequest variable(String name, @Nullable Object value) {
145145
}
146146

147147
@Override
148-
public DefaultRequest extension(String name, Object value) {
148+
public DefaultRequest extension(String name, @Nullable Object value) {
149149
this.extensions.put(name, value);
150150
return this;
151151
}
@@ -270,6 +270,7 @@ void verifyErrors() {
270270
"If expected, please filter them out: " + this.unexpectedErrors,
271271
CollectionUtils.isEmpty(this.unexpectedErrors)));
272272
}
273+
273274
}
274275

275276

@@ -290,7 +291,12 @@ private DefaultResponse(
290291
@Override
291292
public Path path(String path) {
292293
this.delegate.verifyErrors();
293-
return new DefaultPath(path, this.delegate);
294+
return DefaultPath.forPath(null, path, this.delegate);
295+
}
296+
297+
@Override
298+
public Path path(String path, Consumer<Path> pathConsumer) {
299+
return DefaultPath.forNestedPath(null, path, this.delegate, pathConsumer);
294300
}
295301

296302
@Override
@@ -329,6 +335,9 @@ public Traversable satisfy(Consumer<List<ResponseError>> consumer) {
329335
*/
330336
private static final class DefaultPath implements Path {
331337

338+
@Nullable
339+
private final String basePath;
340+
332341
private final String path;
333342

334343
private final ResponseDelegate delegate;
@@ -337,19 +346,20 @@ private static final class DefaultPath implements Path {
337346

338347
private final JsonPathExpectationsHelper pathHelper;
339348

340-
private DefaultPath(String path, ResponseDelegate delegate) {
349+
private DefaultPath(@Nullable String basePath, String path, ResponseDelegate delegate) {
341350
Assert.notNull(path, "`path` is required");
342351
Assert.notNull(delegate, "ResponseContainer is required");
343352

344-
String fullPath = initFullPath(path);
345-
353+
this.basePath = basePath;
346354
this.path = path;
347355
this.delegate = delegate;
356+
357+
String fullPath = initDataJsonPath(this.path);
348358
this.jsonPath = JsonPath.compile(fullPath);
349359
this.pathHelper = new JsonPathExpectationsHelper(fullPath);
350360
}
351361

352-
private static String initFullPath(String path) {
362+
private static String initDataJsonPath(String path) {
353363
if (!StringUtils.hasText(path)) {
354364
path = "$.data";
355365
}
@@ -361,7 +371,12 @@ else if (!path.startsWith("$") && !path.startsWith("data.")) {
361371

362372
@Override
363373
public Path path(String path) {
364-
return new DefaultPath(path, this.delegate);
374+
return forPath(this.basePath, path, this.delegate);
375+
}
376+
377+
@Override
378+
public Path path(String path, Consumer<Path> pathConsumer) {
379+
return forNestedPath(this.basePath, path, this.delegate, pathConsumer);
365380
}
366381

367382
@Override
@@ -390,25 +405,25 @@ public Path pathDoesNotExist() {
390405
@Override
391406
public <D> Entity<D, ?> entity(Class<D> entityType) {
392407
D entity = this.delegate.read(this.jsonPath, new TypeRefAdapter<>(entityType));
393-
return new DefaultEntity<>(entity, this.path, this.delegate);
408+
return new DefaultEntity<>(entity, this.basePath, this.path, this.delegate);
394409
}
395410

396411
@Override
397412
public <D> Entity<D, ?> entity(ParameterizedTypeReference<D> entityType) {
398413
D entity = this.delegate.read(this.jsonPath, new TypeRefAdapter<>(entityType));
399-
return new DefaultEntity<>(entity, this.path, this.delegate);
414+
return new DefaultEntity<>(entity, this.basePath, this.path, this.delegate);
400415
}
401416

402417
@Override
403418
public <D> EntityList<D> entityList(Class<D> elementType) {
404419
List<D> entity = this.delegate.read(this.jsonPath, new TypeRefAdapter<>(List.class, elementType));
405-
return new DefaultEntityList<>(entity, this.path, this.delegate);
420+
return new DefaultEntityList<>(entity, this.basePath, this.path, this.delegate);
406421
}
407422

408423
@Override
409424
public <D> EntityList<D> entityList(ParameterizedTypeReference<D> elementType) {
410425
List<D> entity = this.delegate.read(this.jsonPath, new TypeRefAdapter<>(List.class, elementType));
411-
return new DefaultEntityList<>(entity, this.path, this.delegate);
426+
return new DefaultEntityList<>(entity, this.basePath, this.path, this.delegate);
412427
}
413428

414429
@Override
@@ -439,6 +454,22 @@ private void matchesJson(String expected, boolean strict) {
439454
}
440455
});
441456
}
457+
458+
static Path forPath(@Nullable String basePath, String path, ResponseDelegate delegate) {
459+
String pathToUse = joinPaths(basePath, path);
460+
return new DefaultPath(basePath, pathToUse, delegate);
461+
}
462+
463+
static Path forNestedPath(@Nullable String basePath, String path, ResponseDelegate delegate, Consumer<Path> consumer) {
464+
String pathToUse = joinPaths(basePath, path);
465+
consumer.accept(new DefaultPath(pathToUse, pathToUse, delegate));
466+
return new DefaultPath(basePath, path, delegate);
467+
}
468+
469+
private static String joinPaths(@Nullable String basePath, String path) {
470+
return (basePath != null ? basePath + "." + path : path);
471+
}
472+
442473
}
443474

444475

@@ -449,14 +480,18 @@ private static class DefaultEntity<D, S extends Entity<D, S>> implements Entity<
449480

450481
private final D entity;
451482

483+
@Nullable
484+
private final String basePath;
485+
452486
private final String path;
453487

454488
private final ResponseDelegate delegate;
455489

456-
protected DefaultEntity(D entity, String path, ResponseDelegate delegate) {
490+
protected DefaultEntity(D entity, @Nullable String basePath, String path, ResponseDelegate delegate) {
457491
this.entity = entity;
458-
this.delegate = delegate;
492+
this.basePath = basePath;
459493
this.path = path;
494+
this.delegate = delegate;
460495
}
461496

462497
protected D getEntity() {
@@ -473,7 +508,12 @@ protected String getPath() {
473508

474509
@Override
475510
public Path path(String path) {
476-
return new DefaultPath(path, this.delegate);
511+
return DefaultPath.forPath(this.basePath, path, this.delegate);
512+
}
513+
514+
@Override
515+
public Path path(String path, Consumer<Path> pathConsumer) {
516+
return DefaultPath.forNestedPath(this.basePath, path, this.delegate, pathConsumer);
477517
}
478518

479519
@Override
@@ -528,11 +568,12 @@ private <T extends S> T self() {
528568
/**
529569
* Default {@link EntityList} implementation.
530570
*/
531-
private static final class DefaultEntityList<E> extends DefaultEntity<List<E>, EntityList<E>>
532-
implements EntityList<E> {
571+
@SuppressWarnings("SlowListContainsAll")
572+
private static final class DefaultEntityList<E>
573+
extends DefaultEntity<List<E>, EntityList<E>> implements EntityList<E> {
533574

534-
private DefaultEntityList(List<E> entity, String path, ResponseDelegate delegate) {
535-
super(entity, path, delegate);
575+
private DefaultEntityList(List<E> entity, @Nullable String basePath, String path, ResponseDelegate delegate) {
576+
super(entity, basePath, path, delegate);
536577
}
537578

538579
@Override

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

Lines changed: 18 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -188,18 +188,29 @@ interface Request<T extends Request<T>> {
188188
interface Traversable {
189189

190190
/**
191-
* Switch to a path under the "data" section of the GraphQL response. The path can
192-
* be an operation root type name, e.g. "project", or a nested path such as
193-
* "project.name", or any
191+
* Navigate to a path under the "data" section of the GraphQL response.
192+
* This could be an operation root type name, e.g. "project", or any
194193
* <a href="https://github.com/jayway/JsonPath">JsonPath</a>.
195-
* @param path the path to switch to
196-
* @return spec for asserting the content under the given path
194+
* @param path the path to navigate to
195+
* @return spec with further options at the given path
197196
* @throws AssertionError if the GraphQL response contains
198197
* <a href="https://spec.graphql.org/June2018/#sec-Errors">errors</a> that have
199-
* not be checked via {@link Response#errors()}
198+
* not been checked via {@link Response#errors()}
200199
*/
201200
Path path(String path);
202201

202+
/**
203+
* Variant of {@link #path(String)} with a callback that allows
204+
* inspecting multiple paths under the given path.
205+
* @param path the path to navigate to
206+
* @param pathConsumer callback with a {@link Path} that uses the current
207+
* path as a base path such that any further navigation through this
208+
* {@link Path} is relative to the current path.
209+
* @return spec with further options at the given path
210+
* @since 1.2
211+
*/
212+
Path path(String path, Consumer<Path> pathConsumer);
213+
203214
}
204215

205216
/**

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

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -192,6 +192,25 @@ void entityList() {
192192
assertThat(getActualRequestDocument()).contains(document);
193193
}
194194

195+
@Test
196+
void nestedPath() {
197+
198+
String document = "{me {name, friends}}";
199+
getGraphQlService().setDataAsJson(document,
200+
"{" +
201+
" \"me\":{" +
202+
" \"name\":\"Luke Skywalker\","
203+
+ " \"friends\":[{\"name\":\"Han Solo\"}, {\"name\":\"Leia Organa\"}]" +
204+
" }" +
205+
"}");
206+
207+
graphQlTester().document(document).execute()
208+
.path("me", me -> me
209+
.path("name").entity(String.class).isEqualTo("Luke Skywalker")
210+
.path("friends[0]", f -> f.path("name").entity(String.class).isEqualTo("Han Solo"))
211+
.path("friends[1]", f -> f.path("name").entity(String.class).isEqualTo("Leia Organa")));
212+
}
213+
195214
@Test
196215
void operationNameAndVariables() {
197216

0 commit comments

Comments
 (0)