测试

Spring for GraphQL 提供了针对通过HTTP、WebSocket和RSocket发送的GraphQL请求的专用测试支持,以及直接针对服务器进行测试的支持。spring-doc.cadn.net.cn

要使用此功能,请将 spring-graphql-test 添加到您的构建中。spring-doc.cadn.net.cn

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

GraphQlTester

GraphQlTester 是一个合同,声明了一种独立于底层传输的 GraphQL 请求测试的通用工作流。这意味着无论底层传输是什么,请求都使用相同的 API 进行测试,并且任何与传输相关的配置都在构建时完成。spring-doc.cadn.net.cn

要创建一个执行请求的GraphQlTester,您需要其中一个扩展:spring-doc.cadn.net.cn

要创建一个在服务器端执行测试而无需客户端的GraphQlTesterspring-doc.cadn.net.cn

每个都定义了一个与传输相关的 Builder。所有构建器都从通用的基础 GraphQlTester Builder 继承,并包含适用于所有扩展的选项。spring-doc.cadn.net.cn

HTTP

HttpGraphQlTester 使用 WebTestClient 执行 HTTP 上的 GraphQL 请求,根据 WebTestClient 的配置,可以带有或不带活动服务器。spring-doc.cadn.net.cn

要在 Spring WebFlux 中进行测试,无需使用实际服务器,请指向声明了GraphQL HTTP端点的Spring配置:spring-doc.cadn.net.cn

AnnotationConfigWebApplicationContext context = ...

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

HttpGraphQlTester tester = HttpGraphQlTester.create(client);

要在Spring MVC中进行测试而无需使用实际服务器,请使用MockMvcWebTestClient执行相同的操作:spring-doc.cadn.net.cn

AnnotationConfigWebApplicationContext context = ...

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

HttpGraphQlTester tester = HttpGraphQlTester.create(client);

或测试运行在端口上的活服务器:spring-doc.cadn.net.cn

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

HttpGraphQlTester tester = HttpGraphQlTester.create(client);

一旦 HttpGraphQlTester 被创建,你就可以使用相同的 API 开始 执行请求,而与底层传输无关。如果你需要更改任何特定于传输的细节,可以在现有的 HttpSocketGraphQlTester 上使用 mutate() 来创建一个新的具有自定义设置的实例:spring-doc.cadn.net.cn

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通过共享的WebSocket连接执行GraphQL请求。 它基于Spring WebFlux中的 WebSocketClient 构建,您可以如下创建它:spring-doc.cadn.net.cn

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

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

WebSocketGraphQlTester 是有连接导向型且多路复用的。每个实例为所有请求建立自己的单一共享连接。通常情况下,您只希望每个服务器使用单个实例。spring-doc.cadn.net.cn

一旦 WebSocketGraphQlTester 被创建,你就可以使用相同的 API 开始 执行请求,而与底层传输无关。如果你需要更改任何特定于传输的细节,可以在现有的 WebSocketGraphQlTester 上使用 mutate() 来创建一个新的具有自定义设置的实例:spring-doc.cadn.net.cn

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 提供了一个 stop() 方法,你可以使用它来关闭WebSocket连接,例如在测试运行后。spring-doc.cadn.net.cn

RSocket

RSocketGraphQlTester 使用 RSocketRequester 从 Spring-Messaging 执行 RSocket 上的 GraphQL 请求:spring-doc.cadn.net.cn

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

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

RSocketGraphQlTester 是连接导向的并且是多路复用的。每个实例只为所有请求建立一个单独的、共享的会话。 通常,您只希望在每个服务器上使用单个实例。您可以使用测试器上的 stop() 方法显式地关闭该会话。spring-doc.cadn.net.cn

一旦 RSocketGraphQlTester 被创建,您就可以使用相同的 API 开始 执行请求,而不依赖于底层的传输。spring-doc.cadn.net.cn

ExecutionGraphQlService

很多情况下,测试GraphQL请求只需要在服务器端进行,而不需要使用客户端通过传输协议发送请求。要直接在ExecutionGraphQlService上进行测试,请使用ExecutionGraphQlServiceTester扩展:spring-doc.cadn.net.cn

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

一旦 ExecutionGraphQlServiceTester 被创建,您就可以使用相同的 API 开始 执行请求,而不依赖于底层的传输。spring-doc.cadn.net.cn

ExecutionGraphQlServiceTester.Builder 提供了自定义 ExecutionInput 详细信息的选项:spring-doc.cadn.net.cn

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

WebGraphQlHandler

ExecutionGraphQlService 扩展允许您在无需客户端的情况下在服务器端进行测试。然而,在某些情况下,结合给定的模拟传输输入来涉及服务器端的传输处理是非常有用的。spring-doc.cadn.net.cn

WebGraphQlTester 扩展可以让您通过 WebGraphQlInterceptor 链处理请求,然后再将请求交给 ExecutionGraphQlService 执行。spring-doc.cadn.net.cn

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

此扩展的构建器允许您定义HTTP请求细节:spring-doc.cadn.net.cn

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

一旦WebGraphQlTester被创建,你可以开始使用相同的API 执行请求,而不依赖于底层的传输。spring-doc.cadn.net.cn

构建器

GraphQlTester 定义了一个父级 Builder,用于所有受支持传输的构建器的常见配置选项。 它允许您配置以下内容:spring-doc.cadn.net.cn

  • errorFilter - 一个谓词,用于抑制预期错误,以便您可以检查响应的数据。spring-doc.cadn.net.cn

  • documentSource - 从类路径上的文件或任何其他地方加载请求文档的一种策略。spring-doc.cadn.net.cn

  • responseTimeout - 在请求执行完成之前等待多长时间(以毫秒为单位)后再超时。spring-doc.cadn.net.cn

GraphQlTransport transport = ...
Predicate<ResponseError> errorFilter = ...
ClassPathResource resource = new ClassPathResource("custom-folder/");
DocumentSource documentSource = new ResourceDocumentSource(resource);

GraphQlTester tester = GraphQlTester.builder(transport)
		.documentSource(documentSource)
		.errorFilter(errorFilter)
		.responseTimeout(Duration.ofSeconds(5))
		.build();

请求

一旦您有了GraphQlTester,就可以开始测试请求。下面的示例执行一个项目查询,并使用JsonPath从响应中提取项目发布的版本:spring-doc.cadn.net.cn

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

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

JsonPath 是相对于响应中的 "data" 部分。spring-doc.cadn.net.cn

您也可以在类路径下的"graphql-test/"目录下创建扩展名为.graphql.gql的文档文件,并通过文件名引用它们。spring-doc.cadn.net.cn

例如,在src/main/resources/graphql-test目录下有一个名为projectReleases.graphql的文件,其内容为:
spring-doc.cadn.net.cn

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

然后您可以使用:spring-doc.cadn.net.cn

graphQlTester.documentName("projectReleases") (1)
		.variable("slug", "spring-framework") (2)
		.execute()
		.path("projectReleases.project.releases[*].version")
		.entityList(String.class)
		.hasSizeGreaterThan(1);
1 参照文件"项目"中的文档。
2 设置slug变量。

此方法也适用于加载查询片段。 片段是可重用的字段选择集,可以避免请求文档中的重复内容。 例如,我们可以在多个查询中使用 …​releases 片段:spring-doc.cadn.net.cn

src/main/resources/graphql-documents/projectReleases.graphql
query frameworkReleases {
	project(slug: "spring-framework") {
		name
		...releases
	}
}
query graphqlReleases {
       project(slug: "spring-graphql") {
           name
           ...releases
       }
   }

此片段可以定义在单独的文件中以便重用:spring-doc.cadn.net.cn

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

然后您可以将此片段作为查询文档发送:spring-doc.cadn.net.cn

graphQlTester.documentName("projectReleases") (1)
		.fragmentName("releases") (2)
		.execute()
		.path("frameworkReleases.project.releases[*].version")
		.entityList(String.class)
		.hasSizeGreaterThan(1);
1 从 "projectReleases.graphql" 加载文档
2 从 "releases.graphql" 加载片段并将其附加到文档

"JS GraphQL"插件支持IntelliJ中的GraphQL查询文件,并提供代码完成功能。spring-doc.cadn.net.cn

如果没有请求有任何响应数据,例如mutation,在验证响应无错误时,请使用executeAndVerify而不是executespring-doc.cadn.net.cn

graphQlTester.query(query).executeAndVerify();

错误以获取更多错误处理的细节。spring-doc.cadn.net.cn

嵌套路径

默认情况下,路径相对于GraphQL响应中的“数据”部分。您还可以向下嵌套到一个路径,并检查其相对位置的多个路径,如下所示:spring-doc.cadn.net.cn

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 使用回调来检查相对于“项目”的路径。

订阅

要测试订阅,请调用executeSubscription而不是execute以获得响应流,然后使用来自Project Reactor的StepVerifier来检查该流:spring-doc.cadn.net.cn

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();

订阅功能仅支持通过 WebSocketGraphQlTester, 或在服务器端使用 ExecutionGraphQlServiceWebGraphQlHandler 扩展实现。spring-doc.cadn.net.cn

错误

当您使用 verify() 时,响应中任何以 "errors" 为键的错误都会导致断言失败。要在 verify() 之前抑制特定的错误,请使用错误过滤器。spring-doc.cadn.net.cn

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

可以注册一个错误过滤器在构建器级别,应用于所有测试:spring-doc.cadn.net.cn

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

如果要验证确实存在一个错误,并且与filter相反,如果不存则抛出断言错误,则使用expect代替:spring-doc.cadn.net.cn

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

您还可以通过代码Consumer检查所有错误,并且这样做还会将这些错误标记为已过滤,因此您可以进一步检查响应中的数据:spring-doc.cadn.net.cn

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