Testing

Spring for GraphQL provides dedicated support for testing GraphQL requests over HTTP, WebSocket, and RSocket, as well as for testing directly against a server.

To make use of this, add spring-graphql-test to your build:

  • Gradle

  • Maven

dependencies {
	// ...
	testImplementation 'org.springframework.graphql:spring-graphql-test:1.3.3'
}
<dependencies>
	<!-- ... -->
	<dependency>
		<groupId>org.springframework.graphql</groupId>
		<artifactId>spring-graphql-test</artifactId>
		<version>1.3.3</version>
		<scope>test</scope>
	</dependency>
</dependencies>

GraphQlTester

GraphQlTester is a contract that declares a common workflow for testing GraphQL requests that is independent of the underlying transport. That means requests are tested with the same API no matter what the underlying transport, and anything transport specific is configured at build time.

To create a GraphQlTester that performs requests through a client, you need one of the following extensions:

To create a GraphQlTester that performs tests on the server side, without a client:

Each defines a Builder with options relevant to the transport. All builders extend from a common, base GraphQlTester Builder with options relevant to all extensions.

HTTP

HttpGraphQlTester uses WebTestClient to execute GraphQL requests over HTTP, with or without a live server, depending on how WebTestClient is configured.

To test in Spring WebFlux, without a live server, point to your Spring configuration that declares the GraphQL HTTP endpoint:

AnnotationConfigWebApplicationContext context = ...

WebTestClient client =
		WebTestClient.bindToApplicationContext(context)
				.configureClient()
				.baseUrl("/graphql")
				.build();

HttpGraphQlTester tester = HttpGraphQlTester.create(client);

To test in Spring MVC, without a live server, do the same using MockMvcWebTestClient:

AnnotationConfigWebApplicationContext context = ...

WebTestClient client =
		MockMvcWebTestClient.bindToApplicationContext(context)
				.configureClient()
				.baseUrl("/graphql")
				.build();

HttpGraphQlTester tester = HttpGraphQlTester.create(client);

Or to test against a live server running on a port:

WebTestClient client =
		WebTestClient.bindToServer()
				.baseUrl("http://localhost:8080/graphql")
				.build();

HttpGraphQlTester tester = HttpGraphQlTester.create(client);

Once HttpGraphQlTester is created, you can begin to execute requests using the same API, independent of the underlying transport. If you need to change any transport specific details, use mutate() on an existing HttpSocketGraphQlTester to create a new instance with customized settings:

WebTestClient.Builder clientBuilder =
		WebTestClient.bindToServer()
				.baseUrl("http://localhost:8080/graphql");

HttpGraphQlTester tester = HttpGraphQlTester.builder(clientBuilder)
		.headers((headers) -> headers.setBasicAuth("joe", "..."))
		.build();

// Use tester...

HttpGraphQlTester anotherTester = tester.mutate()
		.headers((headers) -> headers.setBasicAuth("peter", "..."))
		.build();

// Use anotherTester...

WebSocket

WebSocketGraphQlTester executes GraphQL requests over a shared WebSocket connection. It is built using the WebSocketClient from Spring WebFlux and you can create it as follows:

String url = "http://localhost:8080/graphql";
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client).build();

WebSocketGraphQlTester is connection oriented and multiplexed. Each instance establishes its own single, shared connection for all requests. Typically, you’ll want to use a single instance only per server.

Once WebSocketGraphQlTester is created, you can begin to execute requests using the same API, independent of the underlying transport. If you need to change any transport specific details, use mutate() on an existing WebSocketGraphQlTester to create a new instance with customized settings:

URI url = URI.create("ws://localhost:8080/graphql");
WebSocketClient client = new ReactorNettyWebSocketClient();

WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client)
		.headers((headers) -> headers.setBasicAuth("joe", "..."))
		.build();

// Use tester...

WebSocketGraphQlTester anotherTester = tester.mutate()
		.headers((headers) -> headers.setBasicAuth("peter", "..."))
		.build();

// Use anotherTester...

WebSocketGraphQlTester provides a stop() method that you can use to have the WebSocket connection closed, e.g. after a test runs.

RSocket

RSocketGraphQlTester uses RSocketRequester from spring-messaging to execute GraphQL requests over RSocket:

URI url = URI.create("wss://localhost:8080/rsocket");
WebsocketClientTransport transport = WebsocketClientTransport.create(url);

RSocketGraphQlTester client = RSocketGraphQlTester.builder()
		.clientTransport(transport)
		.build();

RSocketGraphQlTester is connection oriented and multiplexed. Each instance establishes its own single, shared session for all requests. Typically, you’ll want to use a single instance only per server. You can use the stop() method on the tester to close the session explicitly.

Once RSocketGraphQlTester is created, you can begin to execute requests using the same API, independent of the underlying transport.

ExecutionGraphQlService

Many times it’s enough to test GraphQL requests on the server side, without the use of a client to send requests over a transport protocol. To test directly against a ExecutionGraphQlService, use the ExecutionGraphQlServiceTester extension:

ExecutionGraphQlService service = ...
ExecutionGraphQlServiceTester tester = ExecutionGraphQlServiceTester.create(service);

Once ExecutionGraphQlServiceTester is created, you can begin to execute requests using the same API, independent of the underlying transport.

ExecutionGraphQlServiceTester.Builder provides an option to customize ExecutionInput details:

ExecutionGraphQlService service = ...
ExecutionId executionId = ExecutionId.generate();
ExecutionGraphQlServiceTester tester = ExecutionGraphQlServiceTester.builder(service)
		.configureExecutionInput((executionInput, builder) -> builder.executionId(executionId).build())
		.build();

WebGraphQlHandler

The ExecutionGraphQlService extension lets you test on the server side, without a client. However, in some cases it’s useful to involve server side transport handling with given mock transport input.

The WebGraphQlTester extension lets you processes request through the WebGraphQlInterceptor chain before handing off to ExecutionGraphQlService for request execution:

WebGraphQlHandler handler = ...
WebGraphQlTester tester = WebGraphQlTester.create(handler);

The builder for this extension allows you to define HTTP request details:

WebGraphQlHandler handler = ...
WebGraphQlTester tester = WebGraphQlTester.builder(handler)
		.headers((headers) -> headers.setBasicAuth("joe", "..."))
		.build();

Once WebGraphQlTester is created, you can begin to execute requests using the same API, independent of the underlying transport.

Builder

GraphQlTester defines a parent Builder with common configuration options for the builders of all extensions. It lets you configure the following:

  • errorFilter - a predicate to suppress expected errors, so you can inspect the data of the response.

  • documentSource - a strategy for loading the document for a request from a file on the classpath or from anywhere else.

  • responseTimeout - how long to wait for request execution to complete before timing out.

Requests

Once you have a GraphQlTester, you can begin to test requests. The below executes a query for a project and uses JsonPath to extract project release versions from the response:

String document =
		"""
		{
			project(slug:"spring-framework") {
				releases {
				version
				}
			}
		}
		""";

graphQlTester.document(document)
		.execute()
		.path("project.releases[*].version")
		.entityList(String.class)
		.hasSizeGreaterThan(1);

The JsonPath is relative to the "data" section of the response.

You can also create document files with extensions .graphql or .gql under "graphql-test/" on the classpath and refer to them by file name.

For example, given a file called projectReleases.graphql in src/main/resources/graphql-test, with content:

query projectReleases($slug: ID!) {
	project(slug: $slug) {
		releases {
			version
		}
	}
}

You can then use:

graphQlTester.documentName("projectReleases") (1)
		.variable("slug", "spring-framework") (2)
		.execute()
		.path("projectReleases.project.releases[*].version")
		.entityList(String.class)
		.hasSizeGreaterThan(1);
1 Refer to the document in the file named "project".
2 Set the slug variable.

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:

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:

src/main/resources/graphql-documents/releases.graphql
fragment releases on Project {
   	releases {
           version
       }
   }

You can then send this fragment along the query document:

graphQlTester.documentName("projectReleases") (1)
		.fragmentName("releases") (2)
		.execute()
		.path("frameworkReleases.project.releases[*].version")
		.entityList(String.class)
		.hasSizeGreaterThan(1);
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.

If a request does not have any response data, e.g. mutation, use executeAndVerify instead of execute to verify there are no errors in the response:

graphQlTester.query(query).executeAndVerify();

See Errors for more details on error handling.

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:

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".

Subscriptions

To test subscriptions, call executeSubscription instead of execute to obtain a stream of responses and then use StepVerifier from Project Reactor to inspect the stream:

Flux<String> greetingFlux = tester.document("subscription { greetings }")
		.executeSubscription()
		.toFlux("greetings", String.class);  // decode at JSONPath

StepVerifier.create(greetingFlux)
		.expectNext("Hi")
		.expectNext("Bonjour")
		.expectNext("Hola")
		.verifyComplete();

Subscriptions are supported only with WebSocketGraphQlTester , or with the server side ExecutionGraphQlService and WebGraphQlHandler extensions.

Errors

When you use verify(), any errors under the "errors" key in the response will cause an assertion failure. To suppress a specific error, use the error filter before verify():

graphQlTester.document(query)
		.execute()
		.errors()
		.filter((error) -> error.getMessage().equals("ignored error"))
		.verify()
		.path("project.releases[*].version")
		.entityList(String.class)
		.hasSizeGreaterThan(1);

You can register an error filter at the builder level, to apply to all tests:

WebGraphQlTester graphQlTester = WebGraphQlTester.builder(handler)
		.errorFilter((error) -> error.getMessage().equals("ignored error"))
		.build();

If you want to verify that an error does exist, and in contrast to filter, throw an assertion error if it doesn’t, then use expect instead:

graphQlTester.document(query)
		.execute()
		.errors()
		.expect((error) -> error.getMessage().equals("expected error"))
		.verify()
		.path("project.releases[*].version")
		.entityList(String.class)
		.hasSizeGreaterThan(1);

You can also inspect all errors through a Consumer, and doing so also marks them as filtered, so you can then also inspect the data in the response:

graphQlTester.document(document)
		.execute()
		.errors()
		.satisfy((errors) ->
				assertThat(errors)
						.anyMatch((error) -> error.getMessage().contains("ignored error"))
		);