diff --git a/spring-graphql-docs/src/docs/asciidoc/client.adoc b/spring-graphql-docs/src/docs/asciidoc/client.adoc new file mode 100644 index 00000000..76fb6da0 --- /dev/null +++ b/spring-graphql-docs/src/docs/asciidoc/client.adoc @@ -0,0 +1,215 @@ +include::attributes.adoc[] + + + + +[[client]] += Client + +Spring for GraphQL includes client support for executing GraphQL requests over HTTP or +over WebSocket. + + + +[[client-graphqlclient]] +== `GraphQlClient` + +`GraphQlClient` defines a common workflow for executing GraphQL requests and +subscriptions that is independent of and agnostic to the underlying transport. To create +an instance, you'll need to start from either the <> or the <> +extensions. + +The main purpose of a `GraphQlClient` extension is to provide a transport specific +`Builder`. There is also a common <> in `GraphQlClient` +with configuration options that apply to any extension. + + + +[[client-httpgraphqlclient]] +=== HTTP + +`HttpGraphQlClient` uses +{spring-framework-ref-docs}/web-reactive.html#webflux-client[WebClient] to execute +GraphQL requests over HTTP. + +[source,java,indent=0,subs="verbatim,quotes"] +---- +WebClient webClient = ... ; +HttpGraphQlClient graphQlClient = HttpGraphQlClient.create(webClient); +---- + +The `HttpGraphQlClient` extension is nothing but a `GraphQlClient` with a specialized +builder. Once created, it exposes the same workflow for request execution that is +independent of the underlying transport. + +This means you can only configure HTTP request details at build time, and +those apply to all requests through that client instance. To change HTTP request +details, use `mutate()` on an existing `HttpGraphQlClient` to create another +instance with different configuration: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + WebClient webClient = ... ; + + HttpGraphQlClient graphQlClient = HttpGraphQlClient.builder(webClient) + .headers(headers -> headers.setBasicAuth("joe", "...")) + .build(); + + // Perform requests with graphQlClient... + + HttpGraphQlClient anotherGraphQlClient = graphQlClient.mutate() + .headers(headers -> headers.setBasicAuth("peter", "...")) + .build(); + + // Perform requests with anotherGraphQlClient... + +---- + + + +[[client-websocketgraphqlclient]] +=== WebSocket + +`WebSocketGraphQlClient` uses +{spring-framework-ref-docs}/web-reactive.html#webflux-websocket-client[WebSocketClient] +from Spring WebFlux to execute GraphQL requests over WebSocket: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + String url = "http://localhost:8080/graphql"; + WebSocketClient client = new ReactorNettyWebSocketClient(); + + WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client).build(); +---- + +Once created, `WebSocketGraphQlClient` exposes the same transport agnostic workflow for +request execution as any `GrahQlClient`. To change any transport details, use `mutate()` +on an existing `WebSocketGraphQlClient` to create another with different configuration: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + URI url = ... ; + WebSocketClient client = ... ; + + WebSocketGraphQlClient graphQlClient = WebSocketGraphQlClient.builder(url, client) + .headers(headers -> headers.setBasicAuth("joe", "...")) + .build(); + + // Use graphQlClient... + + WebSocketGraphQlClient anotherGraphQlClient = graphQlClient.mutate() + .headers(headers -> headers.setBasicAuth("peter", "...")) + .build(); + + // Use anotherGraphQlClient... + +---- + +`WebSocketGraphQlClient` exposes a `start()` method to connect the underlying WebSocket. +This can be used on startup up to prepare for requests, but it is not required. A +connection is established automatically when a request is made. + +`WebSocketGraphQlClient` maintains only one connection at a time that is used in +multiplex style and shared for all requests through the client. If the connection is +lost, it is automatically re-established on the next request. + +`WebSocketGraphQlClient` also exposes a `stop()` method that cancels ongoing +requests and subscriptions, and closes the connection. A stopped client rejects +new requests. Use `start()` to re-establish the connection and allow requests again. + + + +[[client-graphqlclient-builder]] +=== Builder + +`GraphQlClient` defines a parent `Builder` with common configuration options for the +builders of all extensions. Currently, it has lets you configure a `DocumentSource`, +which is a strategy for loading the document for a request by file name. + + + + +[[client-requests]] +== Requests + +Once you have a `GraphQlClient`, you can begin to execute requests. The below executes +a query for a project and uses https://github.com/json-path/JsonPath[JsonPath] to +access the project from the response: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + String document = "{" + + " project(slug:\"spring-framework\") {" + + " name" + + " releases {" + + " version" + + " }"+ + " }" + + "}"; + + Mono projectMono = graphQlClient.document(document) + .execute() + .map(response -> response.toEntity("project", Project.class)); +---- + +The JsonPath is relative to the "data" section of the response. + +You can also create document files with extensions `.graphql` or `.gql` under +`"graphql/"` on the classpath and refer to them by file name. For example, given a file +called `project.graphql` in `src/main/resources/graphql`, with content: + +[source,graphql,indent=0,subs="verbatim,quotes"] +---- + query projectReleases($slug: ID!) { + project(slug: $slug) { + name + releases { + version + } + } + } +---- + +You can then use: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + Mono projectMono = graphQlClient.documentName("project") <1> + .variable("slug", "spring-framework") <2> + .execute() + .map(response -> response.toEntity("project", Project.class)); +---- +<1> Refer to the document in the file named "projectReleases". +<2> Set the `slug` variable. + +[TIP] +==== +The "JS GraphQL" plugin for IntelliJ supports GraphQL query files with code completion. +==== + + + +[[client-subscriptions]] +== Subscriptions + +To start a subscription, call `executeSubscription` instead of `execute` to obtain a +stream of responses rather than a single response: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + Flux greetingFlux = client.document("subscription { greetings }") + .executeSubscription() + .toFlux("greetings", String.class); // decode at JSONPath +---- + +Subscriptions are supported only with <>. + + + + + + + + diff --git a/spring-graphql-docs/src/docs/asciidoc/index.adoc b/spring-graphql-docs/src/docs/asciidoc/index.adoc index b059ef7a..ce61a824 100644 --- a/spring-graphql-docs/src/docs/asciidoc/index.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/index.adoc @@ -1484,13 +1484,16 @@ The Spring for GraphQL repository contains samples for +include::client.adoc[leveloffset=+1] + + + + include::testing.adoc[leveloffset=+1] - - [[samples]] == Samples diff --git a/spring-graphql-docs/src/docs/asciidoc/testing.adoc b/spring-graphql-docs/src/docs/asciidoc/testing.adoc index 25d77b71..9144d596 100644 --- a/spring-graphql-docs/src/docs/asciidoc/testing.adoc +++ b/spring-graphql-docs/src/docs/asciidoc/testing.adoc @@ -1,13 +1,15 @@ include::attributes.adoc[] + + + [[testing]] = Testing -Spring for GraphQL provides dedicated support for testing GraphQL requests. It can send -requests over HTTP or WebSocket using a client. It can also execute requests directly on -the server side. +Spring for GraphQL provides dedicated support for testing GraphQL requests over HTTP or +WebSocket, and for testing GraphQL requests executed directly against a server. -To make use of it, add `spring-graphql-test` to your build: +To make use of this, add `spring-graphql-test` to your build: [source,groovy,indent=0,subs="verbatim,quotes,attributes",role="primary"] .Gradle @@ -32,6 +34,8 @@ dependencies { ---- + + [[testing-graphqltester]] == `GraphQlTester` @@ -44,17 +48,18 @@ To test with a client sending requests to a server over a transport, use the extensions. For server side tests, executed without any client, use the <> or the <> extensions. -The main purpose of an extension is to provide a transport specific `Builder`. There is -also a <> in `GraphQlTester` with common configuration -options that apply to any extension. +The main purpose of a `GraphQlTester` extension is to provide a transport specific +`Builder`. There is also a <> in `GraphQlTester` with +common configuration options that apply to any extension. + [[testing-httpgraphqltester]] === HTTP -`HttpGraphQlTester` wraps a -{spring-framework-ref-docs}/testing.html#webtestclient[WebTestClient] and uses it to -execute GraphQL requests over HTTP, with or without a live server, depending on how +`HttpGraphQlTester` uses +{spring-framework-ref-docs}/testing.html#webtestclient[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 @@ -101,24 +106,24 @@ Or to test against a live server running on a port: ---- The `HttpGraphQlTester` extension is nothing but a `GraphQlTester` with a specialized -builder. Once created, it exposes the same transport agnostic workflow for request -execution. +builder. Once created, it exposes the same workflow for testing requests that is +independent of the underlying transport. This means you can only configure HTTP request details at build time, and -they apply to all requests through that Tester instance. To change HTTP request +those apply to all requests through that Tester instance. To change HTTP request details, use `mutate()` on an existing `HttpGraphQlTester` to create another instance with different configuration: [source,java,indent=0,subs="verbatim,quotes"] ---- HttpGraphQlTester tester = HttpGraphQlTester.builder(clientBuilder) - .httpHeaders(headers -> headers.setBasicAuth("joe", "...")) + .headers(headers -> headers.setBasicAuth("joe", "...")) .build(); // Use tester... HttpGraphQlTester anotherTester = tester.mutate() - .httpHeaders(headers -> headers.setBasicAuth("peter", "...")) + .headers(headers -> headers.setBasicAuth("peter", "...")) .build(); // Use anotherTester... @@ -126,41 +131,48 @@ instance with different configuration: ---- + [[testing-websocketgraphqltester]] === WebSocket -`WebSocketGraphQlTester` wraps the +`WebSocketGraphQlTester` uses {spring-framework-ref-docs}/web-reactive.html#webflux-websocket-client[WebSocketClient] -from Spring WebFlux and uses it to execute GraphQL requests over WebSocket. For example: +from Spring WebFlux to execute GraphQL requests over WebSocket: [source,java,indent=0,subs="verbatim,quotes"] ---- -String url = "http://localhost:8080/graphql"; -WebSocketClient client = new ReactorNettyWebSocketClient(); + String url = "http://localhost:8080/graphql"; + WebSocketClient client = new ReactorNettyWebSocketClient(); -WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client).build(); + WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client).build(); ---- Once created, `WebSocketGraphQlTester` exposes the same transport agnostic workflow for -request execution. To change any transport details, use `mutate()` on an existing -`WebSocketGraphQlTester` to create another with different configuration: +request execution as any `GrahQlTeste`. To change any transport details, use `mutate()` +on an existing `WebSocketGraphQlTester` to create another with different configuration: [source,java,indent=0,subs="verbatim,quotes"] ---- - WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(clientBuilder) - .httpHeaders(headers -> headers.setBasicAuth("joe", "...")) + URI url = ... ; + WebSocketClient client = ... ; + + WebSocketGraphQlTester tester = WebSocketGraphQlTester.builder(url, client) + .headers(headers -> headers.setBasicAuth("joe", "...")) .build(); // Use tester... - WebSocketGraphQlTester adminTester = tester.mutate() - .httpHeaders(headers -> headers.setBasicAuth("peter", "...")) + 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. + + [[testing-graphqlservicetester]] === `GraphQlService` @@ -176,6 +188,7 @@ client to send requests over a transport protocol. To test directly against a ---- + [[testing-webgraphqlhandlertester]] === `WebGraphQlHandler` @@ -199,11 +212,12 @@ The builder for this extension allows you to define HTTP request details: WebGraphQlHandler handler = ... ; WebGraphQlHandlerTester tester = WebGraphQlHandlerTester.builder(handler) - .httpHeaders(headers -> headers.setBasicAuth("joe", "...")) + .headers(headers -> headers.setBasicAuth("joe", "...")) .build(); ---- + [[testing-graphqltester-builder]] === Builder @@ -218,12 +232,14 @@ the classpath or from anywhere else. out. + + [[testing-requests]] == Requests -The below shows an example test that uses -https://github.com/json-path/JsonPath[JsonPath] to extract all project release versions -from the response: +Once you have a `GraphQlTester`, you can begin to test requests. The below executes a +query for a project and uses https://github.com/json-path/JsonPath[JsonPath] to extract +project release versions from the response: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -259,7 +275,7 @@ called `projectReleases.graphql` in `src/main/resources/graphql`, with content: } ---- -You can then re-write the test: +You can then use: [source,java,indent=0,subs="verbatim,quotes"] ---- @@ -270,7 +286,7 @@ You can then re-write the test: .entityList(String.class) .hasSizeGreaterThan(1); ---- -<1> Refer to the document in the file named "projectReleases". +<1> Refer to the document in the file named "project". <2> Set the `slug` variable. [TIP] @@ -286,6 +302,35 @@ instead of `execute` to verify there are no errors in the response: graphQlTester.query(query).executeAndVerify(); ---- +See <> for more details on error handling. + + + + +[[testing-subscriptions]] +== 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: + +[source,java,indent=0,subs="verbatim,quotes"] +---- + Flux 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 <>, or with the server side +<> and <> extensions. + + [[testing-errors]] == Errors @@ -342,27 +387,3 @@ filtered, so you can then also inspect the data in the response: // ... }); ---- - - -[[testing-subscriptions]] -== 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: - -[source,java,indent=0,subs="verbatim,quotes"] ----- - Flux greetingFlux = tester.document("subscription { greetings }") - .executeSubscription() - .toFlux("greetings", String.class); // decode at JSONPath - - StepVerifier.create(greetingFlux) - .expectNext("Hi") - .expectNext("Bonjour") - .expectNext("Hola") - .verifyComplete(); ----- - -You can test subscriptions over WebSocket via <>, or -without a client on the server side, through the <>, or -the <> extensions. \ No newline at end of file