92. 消息传送存根

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

  • Spring Integration

  • Spring 云流

  • Apache Camel

  • Spring AMQP

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

Tip

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

92.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为您提供以下选项来触发消息:

92.1.1 按标签触发

stubFinder.trigger('return_book_1')

92.1.2 按组和工件 ID 触发

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

92.1.3 由工件 ID 触发

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

92.1.4 触发所有消息

stubFinder.trigger()

92.2 存根转轮骆驼

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

92.2.1 将其添加到项目中

在 Classpath 上同时安装 Apache Camel 和 Spring Cloud Contract Stub Runner 就足够了。请记住用@AutoConfigureStubRunnerComments 测试类。

92.2.2 禁用功能

如果您需要禁用此功能,只需传递stubrunner.camel.enabled=false属性即可。

92.2.3 Examples

Stubs structure

让我们假设我们具有以下 Maven 存储库,其中已为camelService应用程序部署了存根。

└── .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

让我们考虑以下 Contract(用 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(无 Importing 消息)

为了通过return_book_1标签触发消息,我们将使用StubTigger接口,如下所示

stubFinder.trigger('return_book_1')

接下来,我们要收听发送到jms:output的消息的输出

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

并且收到的消息将通过以下 assert

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

场景 2(由 Importing 触发输出)

由于已为您设置了 Route,仅向jms:output目标发送一条消息就足够了。

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

接下来,我们要收听发送到jms:output的消息的输出

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

并且收到的消息将通过以下 assert

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

情况 3(Importing 无输出)

由于已为您设置了 Route,仅向jms:output目标发送一条消息就足够了。

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

92.3 Stub Runner 集成

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

92.3.1 将运行器添加到项目

您可以在 Classpath 上同时使用 Spring Integration 和 Spring Cloud Contract Stub Runner。请记住用@AutoConfigureStubRunnerComments 测试类。

92.3.2 禁用功能

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

假定您具有以下integrationService应用程序已部署存根的 Maven 存储库:

└── .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

考虑以下 Contract(编号 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(无 Importing 消息)

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

stubFinder.trigger('return_book_1')

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

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

收到的消息将通过以下 assert:

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

场景 2(由 Importing 触发输出)

由于已为您设置了 Route,因此您可以向output目标发送消息:

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

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

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

收到的消息传递以下 assert:

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

情况 3(Importing 无输出)

由于已为您设置了 Route,因此您可以向input目标发送消息:

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

92.4 存根转轮流

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

Warning

如果 Stub Runner 与 Stream 集成,则首先将messageFromsentTo字符串解析为通道的destination,并且不存在这样的destination,则将目的地解析为通道名。

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"

92.4.1 将运行器添加到项目

您可以在 Classpath 上同时使用 Spring Cloud Stream 和 Spring Cloud Contract Stub Runner。请记住用@AutoConfigureStubRunnerComments 测试类。

92.4.2 禁用功能

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

假设您具有以下 Maven 存储库,其中已为streamService应用程序部署了存根:

└── .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

考虑以下 Contract(编号 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 配置:

stubrunner.repositoryRoot: classpath:m2repo/repository/
stubrunner.ids: org.springframework.cloud.contract.verifier.stubs:streamService:0.0.1-SNAPSHOT:stubs
stubrunner.stubs-mode: remote
spring:
  cloud:
    stream:
      bindings:
        output:
          destination: returnBook
        input:
          destination: bookStorage

server:
  port: 0

debug: true

这些示例使自己适合三种情况:

情况 1(无 Importing 消息)

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

stubFinder.trigger('return_book_1')

要收听发送到destinationreturnBook的 Channels 的消息的输出,请执行以下操作:

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

收到的消息传递以下 assert:

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

场景 2(由 Importing 触发输出)

由于已为您设置了 Route,因此您可以向bookStorage destination发送消息:

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

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

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

收到的消息传递以下 assert:

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

情况 3(Importing 无输出)

由于已为您设置了 Route,因此您可以向output目标发送消息:

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

92.5 Stub Runner Spring AMQP

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

集成尝试独立工作(即,不与正在运行的 RabbitMQ 消息代理进行交互)。它在应用程序上下文中期望RabbitTemplate并将其用作名为@SpyBean的 Spring Boot 测试。结果,它可以使用模仿 Spy 功能来验证和检查应用程序发送的消息。

在消息使用者方面,存根运行器考虑应用程序上下文中所有带有@RabbitListenerComments 的端点和所有SimpleMessageListenerContainer对象。

由于通常将消息发送到 AMQP 中的 Transaction 所,因此消息 Contract 包含 Transaction 所名称作为目的地。另一侧的消息侦听器绑定到队列。绑定将交换连接到队列。如果触发了消息 Contract,则 Spring AMQP 存根运行器集成会在应用程序上下文中查找与该交换匹配的绑定。然后,它从 Spring 交换机收集队列,并尝试查找绑定到这些队列的消息侦听器。将为所有匹配的消息侦听器触发该消息。

如果您需要使用路由键,则足以通过amqp_receivedRoutingKey消息头传递它们。

92.5.1 将运行器添加到项目

您可以在 Classpath 上同时使用 Spring AMQP 和 Spring Cloud Contract Stub Runner 并设置属性stubrunner.amqp.enabled=true。请记住用@AutoConfigureStubRunnerComments 测试类。

Tip

如果您已经在 Classpath 上具有 Stream and Integration,则需要通过设置stubrunner.stream.enabled=falsestubrunner.integration.enabled=false属性来显式禁用它们。

假设您具有以下 Maven 存储库,其中已为spring-cloud-contract-amqp-test应用程序部署了存根。

└── .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 配置:

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

触发讯息

要使用上述 Contract 触发消息,请使用StubTrigger界面,如下所示:

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

该消息的目的地为contract-test.exchange,因此 Spring AMQP 存根运行器集成会查找与此交换有关的绑定。

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

绑定定义绑定队列test.queue。结果,以下侦听器定义将与协定消息匹配并调用。

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

	return container;
}

此外,以下带 Comments 的侦听器将匹配并被调用:

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

Note

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

Spring AMQP 测试配置

为了避免 Spring AMQP 在我们的测试期间尝试连接到正在运行的代理,请配置模拟ConnectionFactory

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

stubrunner:
  amqp:
    mockConnection: false