Add tests for GraphQLTester
See gh-43
This commit is contained in:
@@ -0,0 +1,402 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.graphql.test.query;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.atomic.AtomicReference;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import graphql.ExecutionResult;
|
||||
import graphql.ExecutionResultImpl;
|
||||
import graphql.GraphQLError;
|
||||
import graphql.GraphqlErrorBuilder;
|
||||
import graphql.language.SourceLocation;
|
||||
import okhttp3.mockwebserver.MockResponse;
|
||||
import okhttp3.mockwebserver.MockWebServer;
|
||||
import okhttp3.mockwebserver.RecordedRequest;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.MethodSource;
|
||||
import org.mockito.ArgumentCaptor;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.graphql.WebGraphQLService;
|
||||
import org.springframework.graphql.WebInput;
|
||||
import org.springframework.graphql.WebOutput;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.lang.Nullable;
|
||||
import org.springframework.test.web.reactive.server.WebTestClient;
|
||||
import org.springframework.util.CollectionUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* Tests for {@link GraphQLTester} parameterized to:
|
||||
* <ul>
|
||||
* <li>Connect to {@link MockWebServer} and return a preset HTTP response.
|
||||
* <li>Use mock {@link WebGraphQLService} to return a preset {@link ExecutionResult}.
|
||||
* </ul>
|
||||
*
|
||||
* <p>There is no actual handling via {@link graphql.GraphQL} in either scenario.
|
||||
* The main focus is to verify {@link GraphQLTester} request preparation and
|
||||
* response handling.
|
||||
*/
|
||||
public class GraphQLTesterTests {
|
||||
|
||||
private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper();
|
||||
|
||||
|
||||
public static Stream<GraphQLTesterSetup> argumentSource() {
|
||||
return Stream.of(new MockWebServerSetup(), new MockWebGraphQLServiceSetup());
|
||||
}
|
||||
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("argumentSource")
|
||||
void pathAndValueExistsAndEmptyChecks(GraphQLTesterSetup setup) throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
setup.response("{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
|
||||
|
||||
GraphQLTester.ResponseSpec spec = setup.graphQLTester().query(query).execute();
|
||||
|
||||
spec.path("me.name").pathExists().valueExists().valueIsNotEmpty();
|
||||
spec.path("me.friends").valueIsEmpty();
|
||||
spec.path("hero").pathDoesNotExist().valueDoesNotExist().valueIsEmpty();
|
||||
|
||||
setup.verifyRequest(input -> assertThat(input.getQuery()).contains(query));
|
||||
setup.shutdown();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("argumentSource")
|
||||
void matchesJson(GraphQLTesterSetup setup) throws Exception {
|
||||
|
||||
String query = "{me {name}}";
|
||||
setup.response("{\"me\": {\"name\":\"Luke Skywalker\", \"friends\":[]}}");
|
||||
|
||||
GraphQLTester.ResponseSpec spec = setup.graphQLTester().query(query).execute();
|
||||
|
||||
spec.path("").matchesJson("{\"me\": {\"name\":\"Luke Skywalker\",\"friends\":[]}}");
|
||||
spec.path("me").matchesJson("{\"name\":\"Luke Skywalker\"}");
|
||||
spec.path("me").matchesJson("{\"friends\":[]}"); // lenient match with subset of fields
|
||||
|
||||
assertThatThrownBy(() -> spec.path("me").matchesJsonStrictly("{\"friends\":[]}"))
|
||||
.as("Extended fields should fail in strict mode")
|
||||
.hasMessageContaining("Unexpected: name");
|
||||
|
||||
setup.verifyRequest(input -> assertThat(input.getQuery()).contains(query));
|
||||
setup.shutdown();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("argumentSource")
|
||||
void entity(GraphQLTesterSetup setup) throws Exception {
|
||||
|
||||
String query = "{me {name}}";
|
||||
setup.response("{\"me\": {\"name\":\"Luke Skywalker\"}}");
|
||||
|
||||
GraphQLTester.ResponseSpec spec = setup.graphQLTester().query(query).execute();
|
||||
|
||||
MovieCharacter luke = MovieCharacter.create("Luke Skywalker");
|
||||
MovieCharacter han = MovieCharacter.create("Han Solo");
|
||||
AtomicReference<MovieCharacter> personRef = new AtomicReference<>();
|
||||
|
||||
MovieCharacter actual = spec.path("me").entity(MovieCharacter.class)
|
||||
.isEqualTo(luke)
|
||||
.isNotEqualTo(han)
|
||||
.satisfies(personRef::set)
|
||||
.matches(movieCharacter -> personRef.get().equals(movieCharacter))
|
||||
.isSameAs(personRef.get())
|
||||
.isNotSameAs(luke)
|
||||
.get();
|
||||
|
||||
assertThat(actual.getName()).isEqualTo("Luke Skywalker");
|
||||
|
||||
spec.path("")
|
||||
.entity(new ParameterizedTypeReference<Map<String, MovieCharacter>>() {})
|
||||
.isEqualTo(Collections.singletonMap("me", luke));
|
||||
|
||||
setup.verifyRequest(input -> assertThat(input.getQuery()).contains(query));
|
||||
setup.shutdown();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("argumentSource")
|
||||
void entityList(GraphQLTesterSetup setup) throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
setup.response("{" +
|
||||
" \"me\":{" +
|
||||
" \"name\":\"Luke Skywalker\"," +
|
||||
" \"friends\":[{\"name\":\"Han Solo\"}, {\"name\":\"Leia Organa\"}]" +
|
||||
" }" +
|
||||
"}"
|
||||
);
|
||||
|
||||
GraphQLTester.ResponseSpec spec = setup.graphQLTester().query(query).execute();
|
||||
|
||||
MovieCharacter han = MovieCharacter.create("Han Solo");
|
||||
MovieCharacter leia = MovieCharacter.create("Leia Organa");
|
||||
MovieCharacter jabba = MovieCharacter.create("Jabba the Hutt");
|
||||
|
||||
List<MovieCharacter> actual = spec.path("me.friends").entityList(MovieCharacter.class)
|
||||
.contains(han)
|
||||
.containsExactly(han, leia)
|
||||
.doesNotContain(jabba)
|
||||
.hasSize(2)
|
||||
.hasSizeGreaterThan(1)
|
||||
.hasSizeLessThan(3)
|
||||
.get();
|
||||
|
||||
assertThat(actual).containsExactly(han, leia);
|
||||
|
||||
spec.path("me.friends")
|
||||
.entityList(new ParameterizedTypeReference<MovieCharacter>() {})
|
||||
.containsExactly(han, leia);
|
||||
|
||||
setup.verifyRequest(input -> assertThat(input.getQuery()).contains(query));
|
||||
setup.shutdown();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("argumentSource")
|
||||
void operationNameAndVariables(GraphQLTesterSetup setup) throws Exception {
|
||||
|
||||
String query = "query HeroNameAndFriends($episode: Episode) {" +
|
||||
" hero(episode: $episode) {" +
|
||||
" name" +
|
||||
" }" +
|
||||
"}";
|
||||
|
||||
setup.response("{\"hero\": {\"name\":\"R2-D2\"}}");
|
||||
|
||||
GraphQLTester.ResponseSpec spec = setup.graphQLTester().query(query)
|
||||
.operationName("HeroNameAndFriends")
|
||||
.variable("episode", "JEDI")
|
||||
.variables(map -> map.put("foo", "bar"))
|
||||
.execute();
|
||||
|
||||
spec.path("hero").entity(MovieCharacter.class).isEqualTo(MovieCharacter.create("R2-D2"));
|
||||
|
||||
setup.verifyRequest(input -> {
|
||||
assertThat(input.getQuery()).contains(query);
|
||||
assertThat(input.getOperationName()).isEqualTo("HeroNameAndFriends");
|
||||
assertThat(input.getVariables()).hasSize(2);
|
||||
assertThat(input.getVariables()).containsEntry("episode", "JEDI");
|
||||
assertThat(input.getVariables()).containsEntry("foo", "bar");
|
||||
});
|
||||
setup.shutdown();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("argumentSource")
|
||||
void errorsAssertedIfNotChecked(GraphQLTesterSetup setup) throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
setup.response(GraphqlErrorBuilder.newError()
|
||||
.message("Invalid query")
|
||||
.location(new SourceLocation(1, 2))
|
||||
.build());
|
||||
|
||||
GraphQLTester.ResponseSpec spec = setup.graphQLTester().query(query).execute();
|
||||
|
||||
assertThatThrownBy(() -> spec.path("me")).hasMessageContaining("Response contains GraphQL errors.");
|
||||
|
||||
setup.verifyRequest(input -> assertThat(input.getQuery()).contains(query));
|
||||
setup.shutdown();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("argumentSource")
|
||||
void errorsAssertedOnExecuteAndVerify(GraphQLTesterSetup setup) throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
setup.response(GraphqlErrorBuilder.newError()
|
||||
.message("Invalid query")
|
||||
.location(new SourceLocation(1, 2))
|
||||
.build());
|
||||
|
||||
assertThatThrownBy(() -> setup.graphQLTester().query(query).executeAndVerify())
|
||||
.hasMessageContaining("Response contains GraphQL errors.");
|
||||
|
||||
setup.verifyRequest(input -> assertThat(input.getQuery()).contains(query));
|
||||
setup.shutdown();
|
||||
}
|
||||
|
||||
@ParameterizedTest
|
||||
@MethodSource("argumentSource")
|
||||
void errorsAllowedIfChecked(GraphQLTesterSetup setup) throws Exception {
|
||||
|
||||
String query = "{me {name, friends}}";
|
||||
setup.response(GraphqlErrorBuilder.newError()
|
||||
.message("Invalid query")
|
||||
.location(new SourceLocation(1, 2))
|
||||
.build());
|
||||
|
||||
setup.graphQLTester().query(query).execute()
|
||||
.errorsSatisfy(errors -> {
|
||||
assertThat(errors).hasSize(1);
|
||||
assertThat(errors.get(0).getMessage()).isEqualTo("Invalid query");
|
||||
assertThat(errors.get(0).getLocations()).hasSize(1);
|
||||
assertThat(errors.get(0).getLocations().get(0).getLine()).isEqualTo(1);
|
||||
assertThat(errors.get(0).getLocations().get(0).getColumn()).isEqualTo(2);
|
||||
})
|
||||
.path("me").pathDoesNotExist();
|
||||
|
||||
setup.verifyRequest(input -> assertThat(input.getQuery()).contains(query));
|
||||
setup.shutdown();
|
||||
}
|
||||
|
||||
|
||||
private interface GraphQLTesterSetup {
|
||||
|
||||
GraphQLTester graphQLTester();
|
||||
|
||||
default void response(String data) throws Exception {
|
||||
response(data, Collections.emptyList());
|
||||
}
|
||||
|
||||
default void response(GraphQLError... errors) throws Exception {
|
||||
response(null, Arrays.asList(errors));
|
||||
}
|
||||
|
||||
void response(@Nullable String data, List<GraphQLError> errors) throws Exception;
|
||||
|
||||
void verifyRequest(Consumer<WebInput> consumer) throws Exception;
|
||||
|
||||
default void shutdown() throws Exception {
|
||||
// no-op by default
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
private static class MockWebServerSetup implements GraphQLTesterSetup {
|
||||
|
||||
private final MockWebServer server;
|
||||
|
||||
private final GraphQLTester graphQLTester;
|
||||
|
||||
MockWebServerSetup() {
|
||||
this.server = new MockWebServer();
|
||||
this.graphQLTester = GraphQLTester.create(initWebTestClient(this.server));
|
||||
}
|
||||
|
||||
private static WebTestClient initWebTestClient(MockWebServer server) {
|
||||
String baseUrl = server.url("/graphQL").toString();
|
||||
return WebTestClient.bindToServer().baseUrl(baseUrl).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQLTester graphQLTester() {
|
||||
return this.graphQLTester;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void response(@Nullable String data, List<GraphQLError> errors) throws Exception {
|
||||
StringBuilder sb = new StringBuilder("{");
|
||||
if (StringUtils.hasText(data)) {
|
||||
sb.append("\"data\":").append(data);
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(errors)) {
|
||||
List<Map<String, Object>> errorSpecs = errors.stream()
|
||||
.map(GraphQLError::toSpecification)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
sb.append(StringUtils.hasText(data) ? ", " : "")
|
||||
.append("\"errors\":")
|
||||
.append(OBJECT_MAPPER.writeValueAsString(errorSpecs));
|
||||
}
|
||||
sb.append("}");
|
||||
|
||||
MockResponse response = new MockResponse();
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.setBody(sb.toString());
|
||||
|
||||
this.server.enqueue(response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verifyRequest(Consumer<WebInput> consumer) throws Exception {
|
||||
assertThat(this.server.getRequestCount()).isEqualTo(1);
|
||||
RecordedRequest request = this.server.takeRequest();
|
||||
assertThat(request.getHeader(HttpHeaders.CONTENT_TYPE)).isEqualTo("application/json");
|
||||
|
||||
String content = request.getBody().readUtf8();
|
||||
Map<String, Object> map = new ObjectMapper().readValue(content, new TypeReference<Map<String, Object>>() {});
|
||||
WebInput webInput = new WebInput(request.getRequestUrl().uri(), new HttpHeaders(), map, null);
|
||||
|
||||
consumer.accept(webInput);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void shutdown() throws Exception {
|
||||
this.server.shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
private static class MockWebGraphQLServiceSetup implements GraphQLTesterSetup {
|
||||
|
||||
private final WebGraphQLService service = mock(WebGraphQLService.class);
|
||||
|
||||
private final ArgumentCaptor<WebInput> bodyCaptor = ArgumentCaptor.forClass(WebInput.class);
|
||||
|
||||
private final GraphQLTester graphQLTester;
|
||||
|
||||
public MockWebGraphQLServiceSetup() {
|
||||
this.graphQLTester = GraphQLTester.create(this.service);
|
||||
}
|
||||
|
||||
@Override
|
||||
public GraphQLTester graphQLTester() {
|
||||
return this.graphQLTester;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void response(@Nullable String data, List<GraphQLError> errors) throws Exception {
|
||||
ExecutionResultImpl.Builder builder = new ExecutionResultImpl.Builder();
|
||||
if (data != null) {
|
||||
builder.data(OBJECT_MAPPER.readValue(data, new TypeReference<Map<String, Object>>() {}));
|
||||
}
|
||||
if (!CollectionUtils.isEmpty(errors)) {
|
||||
builder.addErrors(errors);
|
||||
}
|
||||
ExecutionResult result = builder.build();
|
||||
WebOutput output = new WebOutput(mock(WebInput.class), result);
|
||||
when(this.service.execute(this.bodyCaptor.capture())).thenReturn(Mono.just(output));
|
||||
}
|
||||
|
||||
@Override
|
||||
public void verifyRequest(Consumer<WebInput> consumer) {
|
||||
WebInput webInput = this.bodyCaptor.getValue();
|
||||
consumer.accept(webInput);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
/*
|
||||
* Copyright 2002-2021 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.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
package org.springframework.graphql.test.query;
|
||||
|
||||
import org.springframework.lang.Nullable;
|
||||
|
||||
public class MovieCharacter {
|
||||
|
||||
@Nullable
|
||||
private String name;
|
||||
|
||||
public void setName(String name) {
|
||||
this.name = name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getName() {
|
||||
return this.name;
|
||||
}
|
||||
|
||||
public static MovieCharacter create(String name) {
|
||||
MovieCharacter movieCharacter = new MovieCharacter();
|
||||
movieCharacter.setName(name);
|
||||
return movieCharacter;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
if (other == null || getClass() != other.getClass()) {
|
||||
return false;
|
||||
}
|
||||
MovieCharacter movieCharacter = (MovieCharacter) other;
|
||||
return (this.name != null ? this.name.equals(movieCharacter.name) : movieCharacter.name == null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return (this.name != null ? this.name.hashCode() : 0);
|
||||
}
|
||||
}
|
||||
15
spring-graphql-test/src/test/resources/log4j2-test.xml
Normal file
15
spring-graphql-test/src/test/resources/log4j2-test.xml
Normal file
@@ -0,0 +1,15 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<Configuration status="WARN">
|
||||
<Appenders>
|
||||
<Console name="Console" target="SYSTEM_OUT">
|
||||
<PatternLayout pattern="%d{HH:mm:ss.SSS} [%t] %-5level %logger{1.} - %msg%n" />
|
||||
</Console>
|
||||
</Appenders>
|
||||
<Loggers>
|
||||
<Logger name="org.springframework.web" level="debug" />
|
||||
<Logger name="org.springframework.http" level="debug" />
|
||||
<Root level="error">
|
||||
<AppenderRef ref="Console" />
|
||||
</Root>
|
||||
</Loggers>
|
||||
</Configuration>
|
||||
Reference in New Issue
Block a user