86. Stub Runner for Messaging

Stub Runner 可以在 memory 中运行已发布的存根。它可以与以下框架集成:

  • Spring Integration

  • Spring Cloud Stream

  • Apache Camel

  • Spring AMQP

它还提供了与市场上任何其他解决方案集成的切入点。

如果 classpath 上有多个框架 Stub Runner 将需要定义应该使用哪个框架。假设您在 classpath 上同时拥有 AMQP,Spring Cloud Stream 和 Spring Integration。然后你需要设置stubrunner.stream.enabled=falsestubrunner.integration.enabled=false。这样唯一剩下的 framework 就是 Spring AMQP。

86.1 存根触发

要触发消息,请使用StubTrigger接口:

package org.springframework.cloud.contract.stubrunner;

import java.util.Collection;
import java.util.Map;

public interface StubTrigger {

	/**
	 * Triggers an event by a given label for a given {@code groupid:artifactid} notation. You can use only {@code artifactId} too.
	 *
	 * Feature related to messaging.
	 *
	 * @return true - if managed to run a trigger
	 */
	boolean trigger(String ivyNotation, String labelName);

	/**
	 * Triggers an event by a given label.
	 *
	 * Feature related to messaging.
	 *
	 * @return true - if managed to run a trigger
	 */
	boolean trigger(String labelName);

	/**
	 * Triggers all possible events.
	 *
	 * Feature related to messaging.
	 *
	 * @return true - if managed to run a trigger
	 */
	boolean trigger();

	/**
	 * Returns a mapping of ivy notation of a dependency to all the labels it has.
	 *
	 * Feature related to messaging.
	 */
	Map<String, Collection<String>> labels();
}

为方便起见,StubFinder接口扩展StubTrigger,因此您只需要测试中的一个或另一个。

StubTrigger为您提供以下选项来触发消息:

86.1.1 按标签触发

stubFinder.trigger('return_book_1')

86.1.2 由 Group 和 Artifact ID 触发

stubFinder.trigger('org.springframework.cloud.contract.verifier.stubs:camelService', 'return_book_1')

86.1.3 由 Artifact Ids 触发

stubFinder.trigger('camelService', 'return_book_1')

86.1.4 触发所有消息

stubFinder.trigger()

86.2 Stub Runner Camel

Spring Cloud Contract Verifier Stub Runner 的消息传递模块为您提供了一种与 Apache Camel 集成的简便方法。对于提供的 artifacts,它会自动下载存根并注册所需的 routes。

86.2.1 将 Runner 添加到项目中

您可以在 classpath 上同时拥有 Apache Camel 和 Spring Cloud Contract Stub Runner。记得用@AutoConfigureStubRunner注释你的 test class。

86.2.2 禁用该功能

如果需要禁用此功能,请设置stubrunner.camel.enabled=false property。

假设您有以下 Maven repository 以及camelService application 的已部署存根:

└── .m2
    └── repository
        └── io
            └── codearte
                └── accurest
                    └── stubs
                        └── camelService
                            ├── 0.0.1-SNAPSHOT
                            │ ├── camelService-0.0.1-SNAPSHOT.pom
                            │ ├── camelService-0.0.1-SNAPSHOT-stubs.jar
                            │ └── maven-metadata-local.xml
                            └── maven-metadata-local.xml

进一步假设存根包含以下结构:

├── META-INF
│ └── MANIFEST.MF
└── repository
    ├── accurest
    │ ├── bookDeleted.groovy
    │ ├── bookReturned1.groovy
    │ └── bookReturned2.groovy
    └── mappings

考虑以下 contracts(编号1):

Contract.make {
	label 'return_book_1'
	input {
		triggeredBy('bookReturnedTriggered()')
	}
	outputMessage {
		sentTo('jms:output')
		body('''{ "bookName" : "foo" }''')
		headers {
			header('BOOK-NAME', 'foo')
		}
	}
}

现在考虑2

Contract.make {
	label 'return_book_2'
	input {
		messageFrom('jms:input')
		messageBody([
				bookName: 'foo'
		])
		messageHeaders {
			header('sample', 'header')
		}
	}
	outputMessage {
		sentTo('jms:output')
		body([
				bookName: 'foo'
		])
		headers {
			header('BOOK-NAME', 'foo')
		}
	}
}

这些示例适用于三种情况:

场景 1(无输入消息)

要通过return_book_1标签触发消息,请使用StubTigger接口,如下所示:

stubFinder.trigger('return_book_1')

要收听发送到jms:output的消息的输出:

Exchange receivedMessage = camelContext.createConsumerTemplate().receive('jms:output', 5000)

收到的消息传递以下断言:

receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
receivedMessage.in.headers.get('BOOK-NAME') == 'foo'

场景 2(输入触发输出)

由于为您设置了 route,您可以向jms:output目标发送消息:

camelContext.createProducerTemplate().sendBodyAndHeaders('jms:input', new BookReturned('foo'), [sample: 'header'])

您可以收听发送到jms:output的消息的输出:

Exchange receivedMessage = camelContext.createConsumerTemplate().receive('jms:output', 5000)

收到的消息传递以下断言:

receivedMessage != null
assertThatBodyContainsBookNameFoo(receivedMessage.in.body)
receivedMessage.in.headers.get('BOOK-NAME') == 'foo'

场景 3(没有输出的输入)

由于为您设置了 route,您可以向jms:output目标发送消息:

camelContext.createProducerTemplate().sendBodyAndHeaders('jms:delete', new BookReturned('foo'), [sample: 'header'])

86.3 Stub Runner Integration

Spring Cloud Contract Verifier Stub Runner 的消息传递模块为您提供了一种与 Spring Integration 集成的简便方法。对于提供的 artifacts,它会自动下载存根并注册所需的 routes。

86.3.1 将 Runner 添加到项目中

你可以在 classpath 上同时拥有 Spring Integration 和 Spring Cloud Contract Stub Runner。记得用@AutoConfigureStubRunner注释你的 test class。

86.3.2 禁用该功能

如果需要禁用此功能,请设置stubrunner.integration.enabled=false property。

假设您有以下 Maven repository 以及integrationService application 的已部署存根:

└── .m2
    └── repository
        └── io
            └── codearte
                └── accurest
                    └── stubs
                        └── integrationService
                            ├── 0.0.1-SNAPSHOT
                            │ ├── integrationService-0.0.1-SNAPSHOT.pom
                            │ ├── integrationService-0.0.1-SNAPSHOT-stubs.jar
                            │ └── maven-metadata-local.xml
                            └── maven-metadata-local.xml

进一步假设存根包含以下结构:

├── META-INF
│ └── MANIFEST.MF
└── repository
    ├── accurest
    │ ├── bookDeleted.groovy
    │ ├── bookReturned1.groovy
    │ └── bookReturned2.groovy
    └── mappings

考虑以下 contracts(编号1):

Contract.make {
	label 'return_book_1'
	input {
		triggeredBy('bookReturnedTriggered()')
	}
	outputMessage {
		sentTo('output')
		body('''{ "bookName" : "foo" }''')
		headers {
			header('BOOK-NAME', 'foo')
		}
	}
}

现在考虑2

Contract.make {
	label 'return_book_2'
	input {
		messageFrom('input')
		messageBody([
				bookName: 'foo'
		])
		messageHeaders {
			header('sample', 'header')
		}
	}
	outputMessage {
		sentTo('output')
		body([
				bookName: 'foo'
		])
		headers {
			header('BOOK-NAME', 'foo')
		}
	}
}

和以下 Spring Integration Route:

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/integration"
			 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
			 xmlns:beans="http://www.springframework.org/schema/beans"
			 xsi:schemaLocation="http://www.springframework.org/schema/beans
			http://www.springframework.org/schema/beans/spring-beans.xsd
			http://www.springframework.org/schema/integration
			http://www.springframework.org/schema/integration/spring-integration.xsd">

	<!-- REQUIRED FOR TESTING -->
	<bridge input-channel="output"
			output-channel="outputTest"/>

	<channel id="outputTest">
		<queue/>
	</channel>

</beans:beans>

这些示例适用于三种情况:

场景 1(无输入消息)

要通过return_book_1标签触发消息,请使用StubTigger接口,如下所示:

stubFinder.trigger('return_book_1')

要收听发送到output的消息的输出:

Message<?> receivedMessage = messaging.receive('outputTest')

收到的消息将传递以下断言:

receivedMessage != null
assertJsons(receivedMessage.payload)
receivedMessage.headers.get('BOOK-NAME') == 'foo'

场景 2(输入触发输出)

由于为您设置了 route,您可以向output目标发送消息:

messaging.send(new BookReturned('foo'), [sample: 'header'], 'input')

要收听发送到output的消息的输出:

Message<?> receivedMessage = messaging.receive('outputTest')

收到的消息传递以下断言:

receivedMessage != null
assertJsons(receivedMessage.payload)
receivedMessage.headers.get('BOOK-NAME') == 'foo'

场景 3(没有输出的输入)

由于为您设置了 route,您可以向input目标发送消息:

messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete')

86.4 Stub Runner Stream

Spring Cloud Contract Verifier Stub Runner 的消息模块为您提供了一种与 Spring Stream 集成的简便方法。对于提供的 artifacts,它会自动下载存根并注册所需的 routes。

如果 Stub Runner 与流或sentTo Strings 的 integration 首先被解析为 channel 的destination并且不存在destination,则目标将被解析为 channel name。

如果你想使用 Spring Cloud Stream 记住,要在org.springframework.cloud:spring-cloud-stream-test-support上添加一个依赖项。

Maven 的.

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

摇篮.

testCompile "org.springframework.cloud:spring-cloud-stream-test-support"

86.4.1 将 Runner 添加到项目中

你可以在 classpath 上同时拥有 Spring Cloud Stream 和 Spring Cloud Contract Stub Runner。记得用@AutoConfigureStubRunner注释你的 test class。

86.4.2 禁用该功能

如果需要禁用此功能,请设置stubrunner.stream.enabled=false property。

假设您有以下 Maven repository 以及streamService application 的已部署存根:

└── .m2
    └── repository
        └── io
            └── codearte
                └── accurest
                    └── stubs
                        └── streamService
                            ├── 0.0.1-SNAPSHOT
                            │ ├── streamService-0.0.1-SNAPSHOT.pom
                            │ ├── streamService-0.0.1-SNAPSHOT-stubs.jar
                            │ └── maven-metadata-local.xml
                            └── maven-metadata-local.xml

进一步假设存根包含以下结构:

├── META-INF
│ └── MANIFEST.MF
└── repository
    ├── accurest
    │ ├── bookDeleted.groovy
    │ ├── bookReturned1.groovy
    │ └── bookReturned2.groovy
    └── mappings

考虑以下 contracts(编号1):

Contract.make {
	label 'return_book_1'
	input { triggeredBy('bookReturnedTriggered()') }
	outputMessage {
		sentTo('returnBook')
		body('''{ "bookName" : "foo" }''')
		headers { header('BOOK-NAME', 'foo') }
	}
}

现在考虑2

Contract.make {
	label 'return_book_2'
	input {
		messageFrom('bookStorage')
		messageBody([
			bookName: 'foo'
		])
		messageHeaders { header('sample', 'header') }
	}
	outputMessage {
		sentTo('returnBook')
		body([
			bookName: 'foo'
		])
		headers { header('BOOK-NAME', 'foo') }
	}
}

现在考虑以下 Spring configuration:

stubrunner.repositoryRoot: classpath:m2repo/repository/
stubrunner.ids: org.springframework.cloud.contract.verifier.stubs:streamService:0.0.1-SNAPSHOT:stubs

spring:
  cloud:
    stream:
      bindings:
        output:
          destination: returnBook
        input:
          destination: bookStorage

server:
  port: 0

debug: true

这些示例适用于三种情况:

场景 1(无输入消息)

要通过return_book_1标签触发消息,请使用StubTrigger接口,如下所示:

stubFinder.trigger('return_book_1')

要收听发送到为returnBook的 channel 的消息输出:

Message<?> receivedMessage = messaging.receive('returnBook')

收到的消息传递以下断言:

receivedMessage != null
assertJsons(receivedMessage.payload)
receivedMessage.headers.get('BOOK-NAME') == 'foo'

场景 2(输入触发输出)

由于为您设置了 route,您可以向bookStorage destination发送消息:

messaging.send(new BookReturned('foo'), [sample: 'header'], 'bookStorage')

要收听发送到returnBook的消息的输出:

Message<?> receivedMessage = messaging.receive('returnBook')

收到的消息传递以下断言:

receivedMessage != null
assertJsons(receivedMessage.payload)
receivedMessage.headers.get('BOOK-NAME') == 'foo'

场景 3(没有输出的输入)

由于为您设置了 route,您可以向output目标发送消息:

messaging.send(new BookReturned('foo'), [sample: 'header'], 'delete')

86.5 Stub Runner Spring AMQP

Spring Cloud Contract Verifier Stub Runner 的消息传递模块提供了一种与 Spring AMQP 的 Rabbit 模板集成的简便方法。对于提供的 artifacts,它会自动下载存根并注册所需的 routes。

integration 尝试独立工作(即,不与 running RabbitMQ 消息 broker 交互)。它期望 application context 上的RabbitTemplate并将其用作名为@SpyBean的 spring boot 测试。因此,它可以使用 mockito spy 功能来验证和检查 application 发送的消息。

在消息 consumer 端,存根运行器会考虑 application context 上所有@RabbitListener带注释的 endpoints 和所有SimpleMessageListenerContainer objects。

由于消息通常发送到 AMQP 中的交换机,因此消息 contract 包含 exchange name 作为目标。另一侧的消息 listeners 绑定到队列。绑定将交换连接到队列。如果触发了 contracts 消息,则 Spring AMQP 存根运行器 integration 将在 match context 上查找 match 此交换的绑定。然后它从 Spring 交换中收集队列并尝试查找绑定到这些队列的消息 listeners。将为所有匹配的消息 listeners 触发消息。

86.5.1 将 Runner 添加到项目中

你可以在 classpath 上同时拥有 Spring AMQP 和 Spring Cloud Contract Stub Runner 并设置 property stubrunner.amqp.enabled=true。记得用@AutoConfigureStubRunner注释你的 test class。

如果 classpath 上已经有 Stream 和 Integration,则需要通过设置stubrunner.stream.enabled=falsestubrunner.integration.enabled=false properties 来显式禁用它们。

假设您有以下 Maven repository 以及spring-cloud-contract-amqp-test application 的已部署存根。

└── .m2
    └── repository
        └── com
            └── example
                └── spring-cloud-contract-amqp-test
                    ├── 0.4.0-SNAPSHOT
                    │ ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT.pom
                    │ ├── spring-cloud-contract-amqp-test-0.4.0-SNAPSHOT-stubs.jar
                    │ └── maven-metadata-local.xml
                    └── maven-metadata-local.xml

进一步假设存根包含以下结构:

├── META-INF
│ └── MANIFEST.MF
└── contracts
    └── shouldProduceValidPersonData.groovy

考虑以下 contract:

Contract.make {
    // Human readable description
    description 'Should produce valid person data'
    // Label by means of which the output message can be triggered
    label 'contract-test.person.created.event'
    // input to the contract
    input {
        // the contract will be triggered by a method
        triggeredBy('createPerson()')
    }
    // output message of the contract
    outputMessage {
        // destination to which the output message will be sent
        sentTo 'contract-test.exchange'
        headers {
            header('contentType': 'application/json')
            header('__TypeId__': 'org.springframework.cloud.contract.stubrunner.messaging.amqp.Person')
        }
        // the body of the output message
        body ([
                id: $(consumer(9), producer(regex("[0-9]+"))),
                name: "me"
        ])
    }
}

现在考虑以下 Spring configuration:

stubrunner:
  repositoryRoot: classpath:m2repo/repository/
  ids: org.springframework.cloud.contract.verifier.stubs.amqp:spring-cloud-contract-amqp-test:0.4.0-SNAPSHOT:stubs
  amqp:
    enabled: true
server:
  port: 0

触发消息

要使用上面的 contract 触发消息,请使用StubTrigger接口,如下所示:

stubTrigger.trigger("contract-test.person.created.event")

该消息的目标为contract-test.exchange,因此 Spring AMQP 存根运行器 integration 将查找与此交换相关的绑定。

@Bean
public Binding binding() {
	return BindingBuilder.bind(new Queue("test.queue")).to(new DirectExchange("contract-test.exchange")).with("#");
}

binding 定义绑定队列test.queue。因此,以下 listener 定义与 contract 消息匹配并调用。

@Bean
public SimpleMessageListenerContainer simpleMessageListenerContainer(ConnectionFactory connectionFactory,
																		MessageListenerAdapter listenerAdapter) {
	SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
	container.setConnectionFactory(connectionFactory);
	container.setQueueNames("test.queue");
	container.setMessageListener(listenerAdapter);

	return container;
}

此外,以下带注释的 listener 匹配并被调用:

@RabbitListener(bindings = @QueueBinding(
		value = @Queue(value = "test.queue"),
		exchange = @Exchange(value = "contract-test.exchange", ignoreDeclarationExceptions = "true")))
public void handlePerson(Person person) {
	this.person = person;
}

消息直接移交给与匹配SimpleMessageListenerContainer关联的MessageListeneronMessage方法。

Spring AMQP Test Configuration

为了避免 Spring AMQP 在我们的测试期间尝试连接到 running broker,请配置 mock ConnectionFactory

要禁用模拟的 ConnectionFactory,请设置以下 property:stubrunner.amqp.mockConnection=false

stubrunner:
  amqp:
    mockConnection: false