90. Spring Cloud Contract Verifier 消息传递
Spring Cloud Contract Verifier 使您可以验证使用消息传递作为通信手段的应用程序。本文档中显示的所有集成都可以与 Spring 一起使用,但是您也可以创建自己的一个并使用它。
90.1 Integrations
您可以使用以下四种集成配置之一:
-
Apache Camel
-
Spring Integration
-
Spring 云流
-
Spring AMQP
由于我们使用 Spring Boot,因此,如果您已将这些库之一添加到 Classpath 中,则会自动设置所有消息传递配置。
Tip
请记住将@AutoConfigureMessageVerifier
放在生成的测试的 Base Class 上。否则,Spring Cloud Contract Verifier 的消息传递部分将不起作用。
Tip
如果要使用 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>
Gradle.
testCompile "org.springframework.cloud:spring-cloud-stream-test-support"
90.2 手动集成测试
测试使用的主要界面是org.springframework.cloud.contract.verifier.messaging.MessageVerifier
。它定义了如何发送和接收消息。您可以创建自己的实现以实现相同的目标。
在测试中,您可以插入ContractVerifierMessageExchange
以发送和接收遵循 Contract 的消息。然后将@AutoConfigureMessageVerifier
添加到您的测试中。这是一个例子:
@RunWith(SpringTestRunner.class)
@SpringBootTest
@AutoConfigureMessageVerifier
public static class MessagingContractTests {
@Autowired
private MessageVerifier verifier;
...
}
Note
如果您的测试也需要存根,则@AutoConfigureStubRunner
包括消息传递配置,因此您只需要一个 Comments。
90.3 发布者方测试生成
DSL 中有input
或outputMessage
部分会导致在发布者方面创建测试。默认情况下,将创建 JUnit 4 测试。但是,也可以创建 JUnit 5 或 Spock 测试。
我们应考虑 3 种主要情况:
-
方案 1:没有 Importing 消息会生成输出消息。输出消息由应用程序内部的组件(例如,调度程序)触发。
-
方案 2:Importing 消息触发输出消息。
-
方案 3:Importing 消息已被使用,没有输出消息。
Tip
对于不同的消息传递实现,传递给messageFrom
或sentTo
的目的地可能具有不同的含义。对于 Stream 和 Integration ,它首先解析为通道的destination
。然后,如果没有这样的destination
,则将其解析为通道名称。对于 Camel ,这是一个确定的组成部分(例如jms
)。
90.3.1 方案 1:无 Importing 消息
对于给定的 Contract:
Groovy DSL.
def contractDsl = Contract.make {
label 'some_label'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('activemq:output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
messagingContentType(applicationJson())
}
}
}
YAML.
label: some_label
input:
triggeredBy: bookReturnedTriggered
outputMessage:
sentTo: activemq:output
body:
bookName: foo
headers:
BOOK-NAME: foo
contentType: application/json
创建了以下 JUnit 测试:
'''
// when:
bookReturnedTriggered();
// then:
ContractVerifierMessage response = contractVerifierMessaging.receive("activemq:output");
assertThat(response).isNotNull();
assertThat(response.getHeader("BOOK-NAME")).isNotNull();
assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo");
assertThat(response.getHeader("contentType")).isNotNull();
assertThat(response.getHeader("contentType").toString()).isEqualTo("application/json");
// and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
assertThatJson(parsedJson).field("bookName").isEqualTo("foo");
'''
然后将创建以下 Spock 测试:
'''
when:
bookReturnedTriggered()
then:
ContractVerifierMessage response = contractVerifierMessaging.receive('activemq:output')
assert response != null
response.getHeader('BOOK-NAME')?.toString() == 'foo'
response.getHeader('contentType')?.toString() == 'application/json'
and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload))
assertThatJson(parsedJson).field("bookName").isEqualTo("foo")
'''
90.3.2 方案 2:由 Importing 触发的输出
对于给定的 Contract:
Groovy DSL.
def contractDsl = Contract.make {
label 'some_label'
input {
messageFrom('jms:input')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
}
outputMessage {
sentTo('jms:output')
body([
bookName: 'foo'
])
headers {
header('BOOK-NAME', 'foo')
}
}
}
YAML.
label: some_label
input:
messageFrom: jms:input
messageBody:
bookName: 'foo'
messageHeaders:
sample: header
outputMessage:
sentTo: jms:output
body:
bookName: foo
headers:
BOOK-NAME: foo
创建了以下 JUnit 测试:
'''
// given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
"{\\"bookName\\":\\"foo\\"}"
, headers()
.header("sample", "header"));
// when:
contractVerifierMessaging.send(inputMessage, "jms:input");
// then:
ContractVerifierMessage response = contractVerifierMessaging.receive("jms:output");
assertThat(response).isNotNull();
assertThat(response.getHeader("BOOK-NAME")).isNotNull();
assertThat(response.getHeader("BOOK-NAME").toString()).isEqualTo("foo");
// and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.getPayload()));
assertThatJson(parsedJson).field("bookName").isEqualTo("foo");
'''
然后将创建以下 Spock 测试:
"""\
given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
'''{"bookName":"foo"}''',
['sample': 'header']
)
when:
contractVerifierMessaging.send(inputMessage, 'jms:input')
then:
ContractVerifierMessage response = contractVerifierMessaging.receive('jms:output')
assert response !- null
response.getHeader('BOOK-NAME')?.toString() == 'foo'
and:
DocumentContext parsedJson = JsonPath.parse(contractVerifierObjectMapper.writeValueAsString(response.payload))
assertThatJson(parsedJson).field("bookName").isEqualTo("foo")
"""
90.3.3 方案 3:无输出消息
对于给定的 Contract:
Groovy DSL.
def contractDsl = Contract.make {
label 'some_label'
input {
messageFrom('jms:delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}
YAML.
label: some_label
input:
messageFrom: jms:delete
messageBody:
bookName: 'foo'
messageHeaders:
sample: header
assertThat: bookWasDeleted()
创建了以下 JUnit 测试:
'''
// given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
"{\\"bookName\\":\\"foo\\"}"
, headers()
.header("sample", "header"));
// when:
contractVerifierMessaging.send(inputMessage, "jms:delete");
// then:
bookWasDeleted();
'''
然后将创建以下 Spock 测试:
'''
given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
\'\'\'{"bookName":"foo"}\'\'\',
['sample': 'header']
)
when:
contractVerifierMessaging.send(inputMessage, 'jms:delete')
then:
noExceptionThrown()
bookWasDeleted()
'''
90.4Consumer 存根生成
与 HTTP 部分不同,在消息传递中,我们需要使用存根在 JAR 内发布 Groovy DSL。然后在用户端对其进行解析,并创建正确的存根路由。
有关更多信息,请参见第 92 章,用于消息传递的存根运行器部分。
Maven.
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-stream-rabbit</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-stream-test-support</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>Greenwich.BUILD-SNAPSHOT</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
Gradle.
ext {
contractsDir = file("mappings")
stubsOutputDirRoot = file("${project.buildDir}/production/${project.name}-stubs/")
}
// Automatically added by plugin:
// copyContracts - copies contracts to the output folder from which JAR will be created
// verifierStubsJar - JAR with a provided stub suffix
// the presented publication is also added by the plugin but you can modify it as you wish
publishing {
publications {
stubs(MavenPublication) {
artifactId "${project.name}-stubs"
artifact verifierStubsJar
}
}
}