Spring Cloud Stream 支持在不连接到消息传递系统的情况下测试微服务应用程序。Spring中文文档

Spring 集成测试活页夹

Spring Cloud Stream 附带了一个测试绑定器,您可以使用它来测试各种应用程序组件,而无需实际的实际绑定器实现或消息代理。Spring中文文档

这个测试活页夹充当单元测试和集成测试之间的桥梁,它基于 Spring Integration 框架作为 JVM 内消息代理,基本上为您提供了两全其美的体验——一个没有网络的真正活页夹。Spring中文文档

测试活页夹配置

要启用 Spring Integration 测试活页夹,您需要将其添加为依赖项并使用 .@EnableTestBinderSpring中文文档

添加所需的依赖项Spring中文文档

下面是所需的 Maven POM 条目的示例。Spring中文文档

<dependency>
	<groupId>org.springframework.cloud</groupId>
	<artifactId>spring-cloud-stream-test-binder</artifactId>
	<scope>test</scope>
</dependency>

或者对于 build.gradle.ktsSpring中文文档

testImplementation("org.springframework.cloud:spring-cloud-stream-test-binder")

测试活页夹使用情况

现在,您可以将微服务作为简单的单元测试进行测试。 若要启用测试活页夹,请使用 .@EnableTestBinderSpring中文文档

@SpringBootTest
public class SampleStreamTests {

	@Autowired
	private InputDestination input;

	@Autowired
	private OutputDestination output;

	@Test
	public void testEmptyConfiguration() {
		this.input.send(new GenericMessage<byte[]>("hello".getBytes()));
		assertThat(output.receive().getPayload()).isEqualTo("HELLO".getBytes());
	}

	@SpringBootApplication
	@EnableTestBinder
	public static class SampleConfiguration {
		@Bean
		public Function<String, String> uppercase() {
			return v -> v.toUpperCase();
		}
	}
}

如果您需要更多控制或想要在同一测试套件中测试多个配置 您还可以执行以下操作:Spring中文文档

@EnableAutoConfiguration
public static class MyTestConfiguration {
	@Bean
	public Function<String, String> uppercase() {
			return v -> v.toUpperCase();
	}
}

. . .

@Test
public void sampleTest() {
	try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
				TestChannelBinderConfiguration.getCompleteConfiguration(
						MyTestConfiguration.class))
				.run("--spring.cloud.function.definition=uppercase")) {
		InputDestination source = context.getBean(InputDestination.class);
		OutputDestination target = context.getBean(OutputDestination.class);
		source.send(new GenericMessage<byte[]>("hello".getBytes()));
		assertThat(target.receive().getPayload()).isEqualTo("HELLO".getBytes());
	}
}

对于具有多个绑定和/或多个输入和输出的情况,或者只是想明确 要发送到或从接收的目标,并且 和 的 和 方法被覆盖,以允许您提供输入和输出目标的名称。send()receive()InputDestinationOutputDestinationSpring中文文档

请考虑以下示例:Spring中文文档

@EnableAutoConfiguration
public static class SampleFunctionConfiguration {

	@Bean
	public Function<String, String> uppercase() {
		return value -> value.toUpperCase();
	}

	@Bean
	public Function<String, String> reverse() {
		return value -> new StringBuilder(value).reverse().toString();
	}
}

和实际测试Spring中文文档

@Test
public void testMultipleFunctions() {
	try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
			TestChannelBinderConfiguration.getCompleteConfiguration(
					SampleFunctionConfiguration.class))
							.run("--spring.cloud.function.definition=uppercase;reverse")) {

		InputDestination inputDestination = context.getBean(InputDestination.class);
		OutputDestination outputDestination = context.getBean(OutputDestination.class);

		Message<byte[]> inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build();
		inputDestination.send(inputMessage, "uppercase-in-0");
		inputDestination.send(inputMessage, "reverse-in-0");

		Message<byte[]> outputMessage = outputDestination.receive(0, "uppercase-out-0");
		assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes());

		outputMessage = outputDestination.receive(0, "reverse-out-0");
		assertThat(outputMessage.getPayload()).isEqualTo("olleH".getBytes());
	}
}

对于具有其他映射属性(例如)的情况,应使用这些名称。例如,考虑 在前面的测试中,我们显式地将函数的输入和输出映射到并绑定名称:destinationuppercasemyInputmyOutputSpring中文文档

@Test
public void testMultipleFunctions() {
	try (ConfigurableApplicationContext context = new SpringApplicationBuilder(
			TestChannelBinderConfiguration.getCompleteConfiguration(
					SampleFunctionConfiguration.class))
							.run(
							"--spring.cloud.function.definition=uppercase;reverse",
							"--spring.cloud.stream.bindings.uppercase-in-0.destination=myInput",
							"--spring.cloud.stream.bindings.uppercase-out-0.destination=myOutput"
							)) {

		InputDestination inputDestination = context.getBean(InputDestination.class);
		OutputDestination outputDestination = context.getBean(OutputDestination.class);

		Message<byte[]> inputMessage = MessageBuilder.withPayload("Hello".getBytes()).build();
		inputDestination.send(inputMessage, "myInput");
		inputDestination.send(inputMessage, "reverse-in-0");

		Message<byte[]> outputMessage = outputDestination.receive(0, "myOutput");
		assertThat(outputMessage.getPayload()).isEqualTo("HELLO".getBytes());

		outputMessage = outputDestination.receive(0, "reverse-out-0");
		assertThat(outputMessage.getPayload()).isEqualTo("olleH".getBytes());
	}
}

测试 Binder 和 PollableMessageSource

Spring Integration Test Binder 还允许您在使用时编写测试(有关详细信息,请参阅 [使用轮询使用者])。PollableMessageSourceSpring中文文档

不过,需要理解的重要一点是,轮询不是事件驱动的,这是一种公开操作以生成(轮询)消息(单数)的策略。 轮询的频率、使用的线程数量或轮询的位置(消息队列或文件系统)完全取决于您; 换言之,您有责任配置轮询器或线程或消息的实际来源。幸运的是,Spring 有很多抽象来配置它。PollableMessageSourceSpring中文文档

让我们看一下这个例子:Spring中文文档

@Test
public void samplePollingTest() {
	ApplicationContext context = new SpringApplicationBuilder(SamplePolledConfiguration.class)
				.web(WebApplicationType.NONE)
				.run("--spring.jmx.enabled=false", "--spring.cloud.stream.pollable-source=myDestination");
	OutputDestination destination = context.getBean(OutputDestination.class);
	System.out.println("Message 1: " + new String(destination.receive().getPayload()));
	System.out.println("Message 2: " + new String(destination.receive().getPayload()));
	System.out.println("Message 3: " + new String(destination.receive().getPayload()));
}

@EnableTestBinder
@EnableAutoConfiguration
public static class SamplePolledConfiguration {
	@Bean
	public ApplicationRunner poller(PollableMessageSource polledMessageSource, StreamBridge output, TaskExecutor taskScheduler) {
		return args -> {
			taskScheduler.execute(() -> {
				for (int i = 0; i < 3; i++) {
					try {
						if (!polledMessageSource.poll(m -> {
							String newPayload = ((String) m.getPayload()).toUpperCase();
							output.send("myOutput", newPayload);
						})) {
							Thread.sleep(2000);
						}
					}
					catch (Exception e) {
						// handle failure
					}
				}
			});
		};
	}
}

上面的(非常基本的)示例将以 2 秒的间隔生成 3 条消息,将它们发送到输出目标,此活页夹将发送到我们检索它们的位置(对于任何断言)。 目前,它打印以下内容:SourceOutputDestinationSpring中文文档

Message 1: POLLED DATA
Message 2: POLLED DATA
Message 3: POLLED DATA

正如你所看到的,数据是一样的。这是因为这个活页夹定义了实际 - 源代码的默认实现 使用操作从中轮询消息。虽然对于大多数测试方案来说已经足够了,但在某些情况下,你可能需要 来定义你自己的 .为此,只需在测试配置中配置一个类型的 Bean,提供您自己的 Bean 消息溯源的实现。MessageSourcepoll()MessageSourceMessageSourceSpring中文文档

下面是示例:Spring中文文档

@Bean
public MessageSource<?> source() {
	return () -> new GenericMessage<>("My Own Data " + UUID.randomUUID());
}

呈现以下输出;Spring中文文档

Message 1: MY OWN DATA 1C180A91-E79F-494F-ABF4-BA3F993710DA
Message 2: MY OWN DATA D8F3A477-5547-41B4-9434-E69DA7616FEE
Message 3: MY OWN DATA 20BF2E64-7FF4-4CB6-A823-4053D30B5C74
不要命名这个 bean,因为它将与同名(不同类型的)的 bean 冲突 由 Spring Boot 出于不相关原因提供。messageSource
不要命名这个 bean,因为它将与同名(不同类型的)的 bean 冲突 由 Spring Boot 出于不相关原因提供。messageSource