diff --git a/spring-graphql-docs/src/docs/asciidoc/includes/testing.adoc b/spring-graphql-docs/src/docs/asciidoc/includes/testing.adoc index fab1dbec..3d0c7d5c 100644 --- a/spring-graphql-docs/src/docs/asciidoc/includes/testing.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/includes/testing.adoc @@ -359,6 +359,26 @@ See <> for more details on error handling. +[[testing.requests.nestedPaths]] +=== Nested Paths + +By default, paths are relative to the "data" section of the GraphQL response. You can also +nest down to a path, and inspect multiple paths relative to it as follows: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + graphQlTester.document(document) + .execute() + .path("project", project -> project // <1> + .path("name").entity(String.class).isEqualTo("spring-framework") + .path("releases[*].version").entityList(String.class).hasSizeGreaterThan(1)); +---- + +<1> Use a callback to inspect paths relative to "project". + + + + [[testing.subscriptions]] == Subscriptions diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java index 003f7c3b..2faeeb85 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/DefaultGraphQlTester.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -145,7 +145,7 @@ final class DefaultGraphQlTester implements GraphQlTester { } @Override - public DefaultRequest extension(String name, Object value) { + public DefaultRequest extension(String name, @Nullable Object value) { this.extensions.put(name, value); return this; } @@ -270,6 +270,7 @@ final class DefaultGraphQlTester implements GraphQlTester { "If expected, please filter them out: " + this.unexpectedErrors, CollectionUtils.isEmpty(this.unexpectedErrors))); } + } @@ -290,7 +291,12 @@ final class DefaultGraphQlTester implements GraphQlTester { @Override public Path path(String path) { this.delegate.verifyErrors(); - return new DefaultPath(path, this.delegate); + return DefaultPath.forPath(null, path, this.delegate); + } + + @Override + public Path path(String path, Consumer pathConsumer) { + return DefaultPath.forNestedPath(null, path, this.delegate, pathConsumer); } @Override @@ -329,6 +335,9 @@ final class DefaultGraphQlTester implements GraphQlTester { */ private static final class DefaultPath implements Path { + @Nullable + private final String basePath; + private final String path; private final ResponseDelegate delegate; @@ -337,19 +346,20 @@ final class DefaultGraphQlTester implements GraphQlTester { private final JsonPathExpectationsHelper pathHelper; - private DefaultPath(String path, ResponseDelegate delegate) { + private DefaultPath(@Nullable String basePath, String path, ResponseDelegate delegate) { Assert.notNull(path, "`path` is required"); Assert.notNull(delegate, "ResponseContainer is required"); - String fullPath = initFullPath(path); - + this.basePath = basePath; this.path = path; this.delegate = delegate; + + String fullPath = initDataJsonPath(this.path); this.jsonPath = JsonPath.compile(fullPath); this.pathHelper = new JsonPathExpectationsHelper(fullPath); } - private static String initFullPath(String path) { + private static String initDataJsonPath(String path) { if (!StringUtils.hasText(path)) { path = "$.data"; } @@ -361,7 +371,12 @@ final class DefaultGraphQlTester implements GraphQlTester { @Override public Path path(String path) { - return new DefaultPath(path, this.delegate); + return forPath(this.basePath, path, this.delegate); + } + + @Override + public Path path(String path, Consumer pathConsumer) { + return forNestedPath(this.basePath, path, this.delegate, pathConsumer); } @Override @@ -390,25 +405,25 @@ final class DefaultGraphQlTester implements GraphQlTester { @Override public Entity entity(Class entityType) { D entity = this.delegate.read(this.jsonPath, new TypeRefAdapter<>(entityType)); - return new DefaultEntity<>(entity, this.path, this.delegate); + return new DefaultEntity<>(entity, this.basePath, this.path, this.delegate); } @Override public Entity entity(ParameterizedTypeReference entityType) { D entity = this.delegate.read(this.jsonPath, new TypeRefAdapter<>(entityType)); - return new DefaultEntity<>(entity, this.path, this.delegate); + return new DefaultEntity<>(entity, this.basePath, this.path, this.delegate); } @Override public EntityList entityList(Class elementType) { List entity = this.delegate.read(this.jsonPath, new TypeRefAdapter<>(List.class, elementType)); - return new DefaultEntityList<>(entity, this.path, this.delegate); + return new DefaultEntityList<>(entity, this.basePath, this.path, this.delegate); } @Override public EntityList entityList(ParameterizedTypeReference elementType) { List entity = this.delegate.read(this.jsonPath, new TypeRefAdapter<>(List.class, elementType)); - return new DefaultEntityList<>(entity, this.path, this.delegate); + return new DefaultEntityList<>(entity, this.basePath, this.path, this.delegate); } @Override @@ -439,6 +454,22 @@ final class DefaultGraphQlTester implements GraphQlTester { } }); } + + static Path forPath(@Nullable String basePath, String path, ResponseDelegate delegate) { + String pathToUse = joinPaths(basePath, path); + return new DefaultPath(basePath, pathToUse, delegate); + } + + static Path forNestedPath(@Nullable String basePath, String path, ResponseDelegate delegate, Consumer consumer) { + String pathToUse = joinPaths(basePath, path); + consumer.accept(new DefaultPath(pathToUse, pathToUse, delegate)); + return new DefaultPath(basePath, path, delegate); + } + + private static String joinPaths(@Nullable String basePath, String path) { + return (basePath != null ? basePath + "." + path : path); + } + } @@ -449,14 +480,18 @@ final class DefaultGraphQlTester implements GraphQlTester { private final D entity; + @Nullable + private final String basePath; + private final String path; private final ResponseDelegate delegate; - protected DefaultEntity(D entity, String path, ResponseDelegate delegate) { + protected DefaultEntity(D entity, @Nullable String basePath, String path, ResponseDelegate delegate) { this.entity = entity; - this.delegate = delegate; + this.basePath = basePath; this.path = path; + this.delegate = delegate; } protected D getEntity() { @@ -473,7 +508,12 @@ final class DefaultGraphQlTester implements GraphQlTester { @Override public Path path(String path) { - return new DefaultPath(path, this.delegate); + return DefaultPath.forPath(this.basePath, path, this.delegate); + } + + @Override + public Path path(String path, Consumer pathConsumer) { + return DefaultPath.forNestedPath(this.basePath, path, this.delegate, pathConsumer); } @Override @@ -528,11 +568,12 @@ final class DefaultGraphQlTester implements GraphQlTester { /** * Default {@link EntityList} implementation. */ - private static final class DefaultEntityList extends DefaultEntity, EntityList> - implements EntityList { + @SuppressWarnings("SlowListContainsAll") + private static final class DefaultEntityList + extends DefaultEntity, EntityList> implements EntityList { - private DefaultEntityList(List entity, String path, ResponseDelegate delegate) { - super(entity, path, delegate); + private DefaultEntityList(List entity, @Nullable String basePath, String path, ResponseDelegate delegate) { + super(entity, basePath, path, delegate); } @Override diff --git a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java index 8c382125..124b55f6 100644 --- a/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java +++ b/spring-graphql-test/src/main/java/org/springframework/graphql/test/tester/GraphQlTester.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -188,18 +188,29 @@ public interface GraphQlTester { interface Traversable { /** - * Switch to a path under the "data" section of the GraphQL response. The path can - * be an operation root type name, e.g. "project", or a nested path such as - * "project.name", or any + * Navigate to a path under the "data" section of the GraphQL response. + * This could be an operation root type name, e.g. "project", or any * JsonPath. - * @param path the path to switch to - * @return spec for asserting the content under the given path + * @param path the path to navigate to + * @return spec with further options at the given path * @throws AssertionError if the GraphQL response contains * errors that have - * not be checked via {@link Response#errors()} + * not been checked via {@link Response#errors()} */ Path path(String path); + /** + * Variant of {@link #path(String)} with a callback that allows + * inspecting multiple paths under the given path. + * @param path the path to navigate to + * @param pathConsumer callback with a {@link Path} that uses the current + * path as a base path such that any further navigation through this + * {@link Path} is relative to the current path. + * @return spec with further options at the given path + * @since 1.2 + */ + Path path(String path, Consumer pathConsumer); + } /** diff --git a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java index dddf261d..e8a0b324 100644 --- a/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java +++ b/spring-graphql-test/src/test/java/org/springframework/graphql/test/tester/GraphQlTesterTests.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2022 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -192,6 +192,25 @@ public class GraphQlTesterTests extends GraphQlTesterTestSupport { assertThat(getActualRequestDocument()).contains(document); } + @Test + void nestedPath() { + + String document = "{me {name, friends}}"; + getGraphQlService().setDataAsJson(document, + "{" + + " \"me\":{" + + " \"name\":\"Luke Skywalker\"," + + " \"friends\":[{\"name\":\"Han Solo\"}, {\"name\":\"Leia Organa\"}]" + + " }" + + "}"); + + graphQlTester().document(document).execute() + .path("me", me -> me + .path("name").entity(String.class).isEqualTo("Luke Skywalker") + .path("friends[0]", f -> f.path("name").entity(String.class).isEqualTo("Han Solo")) + .path("friends[1]", f -> f.path("name").entity(String.class).isEqualTo("Leia Organa"))); + } + @Test void operationNameAndVariables() {