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 中有inputoutputMessage部分会导致在发布者方面创建测试。默认情况下,将创建 JUnit 4 测试。但是,也可以创建 JUnit 5 或 Spock 测试。

我们应考虑 3 种主要情况:

  • 方案 1:没有 Importing 消息会生成输出消息。输出消息由应用程序内部的组件(例如,调度程序)触发。

  • 方案 2:Importing 消息触发输出消息。

  • 方案 3:Importing 消息已被使用,没有输出消息。

Tip

对于不同的消息传递实现,传递给messageFromsentTo的目的地可能具有不同的含义。对于 StreamIntegration ,它首先解析为通道的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
		}
	}
}