Spring Cloud Contract十全大补丸

Posted by Kriz on 2018-02-05

前言

在一个微服务架构中,各个微服务相对独立,但对其进行测试要比传统应用测试的难度更大。主要的原因是单个微服务往往依赖于其他服务,而其依赖的服务又进一步依赖更多服务,形成复杂的耦合关系。

img按照传统的思维方式,针对这种情况有两种常见的测试方式:

  1. 部署所有的服务来完成端到端测试

    这是一种有效的测试方式。它可以最大限度模拟生产环境,真实地测试服务端的交互。

    但这种方式的弊端也显而易见:部署过程复杂费时,整个流程跑下来常常需要很久。此外,还有一个不可忽视的问题:一旦报错,因为错误无法准确定位,调错的过程会非常艰难

  2. 消费端Mock所需的服务

    这是一种传统应用中普遍使用的方式。它速度很快,而且基本没有依赖。

    问题在于,mock无法反映真实的接口,这个问题在微服务架构中尤其具有严重性。因此会很容易出现这样的情况:本地test全部通过,但一旦开始集成测试,就会出现大量问题。换句话说,消费者永远不知道服务者什么时候改变了接口,服务者也永远不知道接口改动是否会破坏消费端的服务

为了避免这些问题,基于契约的测试方式(contract based testing)应运而生。这种方式的主旨是:确定一份契约,供消费端和服务端共同使用,契约对接口进行规范;整个流程需要消费端和服务端协作完成、共同维护。

相对于传统方式,契约测试的步骤略显繁琐,且对两端的沟通提出了更高的要求。但它同时解决了传统测试方式的遗留问题,尤其对于交互关系比较复杂的微服务架构体系非常适用。

Spring Cloud Contract(以下简称Contract)即是Spring生态圈中进行契约测试的插件。但目前网络上关于该插件的资料非常匮乏,且一些文章有错漏、误导之处,因此我结合Spring官方文档、demo、个人踩过的坑及一些资深使用者的经验分享,力求以尽量通俗的方式集合成为此篇个人总结,后来者也可以此作为入门参考。

CDC及其流程

传统的契约测试往往由服务者驱动——确认消费者的需求并实现它们。而现在我们将更多地讨论CDC,即消费者驱动的契约测试(Consumer Driven Contracts)。

顾名思义,CDC的原则就是消费者驱动。由消费者来编写契约、改变契约,因为消费者最了解需求。如果服务者无论如何想要改变契约,也需要通知消费者修改。它在绝大多数情况下都适用。

一个典型的CDC流程如下所示。因为消费端和服务端将共同参与其中,因此请务必注意每一步是谁在做。这里是全篇的核心,每一步的内容和目的都很重要。

  1. 消费者在消费端编写相关功能的单元测试,以及需求的实现

    消费者编写相关功能的单元测试及需求实现。建议采用测试驱动开发,也就是遵循TDD原则来完成这部分的内容。代码编写完成后,因为这时微服务之间的通信还无法进行,所以测试跑不通。

  2. 消费者撰写完整的、涵盖全部需求的契约

    一般来说,由消费者写好“契约”并放置在某个约定好的位置。例如,根据Contract的默认配置,契约需要被放置在服务端的src/test/resources/contracts目录下。在这种情况下,消费者需要clone服务端,将契约放进去。

  3. 消费者和服务者共同确认契约,消费者发布stub供测试使用

    在契约被最终确认后,消费者可以使用它生成stub。消费者将可以通过运行这些stub来模拟运行服务,从而进行消费端的测试。如果逻辑无误,在这一步应该能够通过所有相关的消费端单元测试。
    img

  4. 服务者在服务端针对契约编写测试和需求的实现

    服务者针对契约编写需求的实现。针对契约的测试完成后,一旦契约发生变化,测试将无法通过,此时服务者需要修正测试代码和实现代码来契合契约内容,并重新生成新的stub供消费端继续使用。当然,一般情况下,契约的任何变化都需要消费者和服务者双方共同确认。

HTTP契约的内容

在开始一个工作流程实例之前,我们先来说说契约。作为入门,这里以简单易懂的HTTP契约为例;若想了解与message工作流有关的契约,请查看本文的后半部分。

一份HTTP契约,应当包含requestresponse两个主要的部分。

很容易理解,request部分是消费端发送的请求,包含HTTP动词、路由、请求头、请求体等;response则是服务端返回的响应信息,包含状态信息、响应头、响应体等。而这些信息,只要不是默认值都应当体现在契约中。

在Contract中,契约使用GroovyDSL来完成。它类似于Java,但提供一些更先进的特性。如果不想用Groovy,也可以使用YAML

一个简单的契约参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
org.springframework.cloud.contract.spec.Contract.make {
request {
method 'POST'
url '/beer'
body("""
{
"id":"123457",
"age":21,
"name":"Kevinz"
}
""")
headers {
contentType('application/json')
}
}
response {
status 200
body([
checkedStatus: "OK",
"orderStatus": true
])
headers {
contentType('application/json')
}
}
}

实例:一个简单的应用

接下来,我们会接触到Contract中两个重要的部分:Spring Cloud Contract VerifierSpring Cloud Contract Stub Runner。前者是一个CDC框架,它可以使基于Spring Cloud项目的契约测试流程更加简单、易于控制;后者则用于自动生成、下载和部署stub。

我们按照上文提到的CDC流程来进行一个小应用的开发。内容很简单:消费端用于请求购买啤酒,服务端进行判断:如果年龄超过18岁则出售啤酒,反之则拒绝。

这个Demo的Repo:https://github.com/kevinzwithu/ContractDemo

首先,我们构建两个Maven项目,分别作为消费端和服务端。

第一步:消费者编写测试和实现

在消费端引入pom依赖:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud-dependencies.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-wiremock</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-stub-runner</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

其中,wiremock作为stub的服务器,而stub-runner用于自动下载和运行stub。

接下来,我们编写覆盖所需功能代码的测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Test
public void shouldBeRejectedDueToAgeLimited() {
// given:
OrderBeerApplication application = new OrderBeerApplication(new Consumer("123456", 15, "Greenberg", 1, 3));
// when:
OrderBeerApplicationResult orderResult = service.orderApplication(application);
// then:
assertThat(orderResult.getOrderBeerApplicationStatus()).isEqualTo(OrderBeerApplicationStatus.ORDER_REJECTED);
assertThat(orderResult.getRejectReason()).isEqualTo("Too young to get beer");
}
@Test
public void shouldBeAllowed() {
//given:
OrderBeerApplication application = new OrderBeerApplication(new Consumer("123457", 21, "Kevinz", 1, 2));
// when:
OrderBeerApplicationResult orderResult = service.orderApplication(application);
// then:
assertThat(orderResult.getOrderBeerApplicationStatus()).isEqualTo(OrderBeerApplicationStatus.ORDER_ALLOWED);
}

因为还没有任何实现,所以测试全部无法通过,接下来我们针对这些测试来完成实现。

在完成所有实现部分的内容之后,再次进行测试还是无法通过。但这次是因为还没有判断年龄的卖酒人(服务端),因此我们要完成一份契约,明确地指出我们会提供的信息以及希望得到的信息,从而根据契约生成stub来使测试通过,同时这份契约还能约束服务端不要乱改接口。

第二步:消费者编写契约

因为有两个测试用例,所以我们需要提供两份契约。请尽量编写覆盖所有测试情况的契约。契约可以参考这里

契约写好之后,在服务端pom中引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
<dependencyManagement>
<dependencies>
<!-- If you're adding this dependency explicitly you have to
add it *BEFORE* the Release Train BOM-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-dependencies</artifactId>
<version>1.2.1.RELEASE</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>1.2.1.RELEASE</version>
<extensions>true</extensions>
<configuration>
<!-- Provide the base class for your auto-generated tests -->
<baseClassForTests>cn.kevinz.ProviderTest</baseClassForTests>
</configuration>
</plugin>
<plugins>
</build>

随后,消费者还需要和服务端共同确认一些配置,也就是spring-cloud-contract-maven-plugin插件的configuration标签中的元素。可选项包括:

  • testMode:定义验收测试的模式,默认是MockMvc,可以替换成JaxRsClientExplicit
  • contractsDirectory:定义契约存放的位置。默认是/src/test/resources/contracts
  • testFramework:定义测试框架,默认是JUnit。好像也支持Spock,未证实。

还有很多涉及到契约仓库和服务端自动生成测试的基类等的配置选项,可以查阅官方文档

第三步:确认契约并发布stub

消费者和服务者确认契约无误后,就可以生成stub了。

默认情况下,stub runner会在install之后生成stub和针对契约的测试。消费者无须考虑契约测试,只生成stub即可:

1
mvn clean install -DskipTests

执行完成后,可以看到在target目录下出现了名为provider-0.0.1-SNAPSHOT-stubs的jar包(具体文件名根据项目决定),这就是我们所需要的stub了。

接下来,消费者想要让这个stub运行起来模拟服务,来让测试全部跑通。我们可以在测试类打上@AutoConfigureStubRunner标签:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@...
@AutoConfigureStubRunner(ids = { "cn.kevinz:provider:+:stubs:8080" }, stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class OrderBeerApplicationTests {
...
@Test
public void shouldBeRejectedDueToAgeLimited() {
...
}
@Test
public void shouldBeAllowed() {
...
}
}

其中ids指定stub的位置(格式是:groupId:artifactId:version:classifier:port,加号表示自动下载最新版本),多个stub可以用逗号分开;stubMode设定为LOCAL表示使用本地stub,这个参数在旧版似乎是叫做offline的布尔型。

运行测试,此时所有的测试都应该通过。

第四步:针对契约的测试和实现

服务者执行

1
mvn clean install

将会在自动生成stub的同时,生成并运行测试。默认情况下,自动生成的测试将会被存放在/target/generated-test-sources中。

因为没有实现或者实现不全,测试基本会失败。这时需要编写覆盖契约测试的实现代码,直到测试通过。

CAUTION!

这里会涉及到契约测试基类的问题,服务端必须要提供至少一个基类才能够使verifier自动生成契约测试。

个人认为官方文档写得不是很清楚,仅供参考。在阅读部分开发者的源码后,我发现使用读入本地json文件检验是否和契约匹配的方法十分普遍。如果对这个方法感兴趣,请参考本文的最后一个部分。

这之后,如果契约出现变化,在mvn install后契约测试将很可能会不通过,需要服务端调整实现代码。到这里,我们就已经实现了期望:对单个微服务解耦完成快速的测试,并且能够防止两端接口不一致引发的诸多问题。

可用性优化

了解了基本的使用方式后,还有一些针对项目本身特点的、能够提高可用性的方式。

动态值匹配

我们在之前的契约中,传入了这样一个请求体:

1
2
3
4
5
{
"id":"123457",
"age":21,
"name":"Kevinz"
}

但很多情况下,我们很难以hard code的形式传值,比如随机生成的时间和UUID:

1
2
3
4
5
{
"time" : "2016-10-10 20:10:15",
"id" : "9febab1c-6f36-4a0b-88d6-3b6a6d81cd4a",
"body" : "foo"
}

响应自然也是这样。

为了解决这个问题,Contract允许以动态的方式传值(Dynamic Value)。可以使用value()方法(或者语法糖$()):

1
2
3
value(consumer(...), producer(...))
value(stub(...), test(...))
value(client(...), server(...))

这三者是相同的,以第一条为例:consumer()方法中传入的动态值将用于stub中,而provider()方法中传入的动态值将用于契约测试中。一般而言,动态值使用regex()方法,即正则表达式来匹配。像这样:

1
2
3
4
5
6
7
8
9
request {
method 'GET'
url '/someUrl'
body([
time: value(consumer(regex('[0-9]{4}-[0-9]{2}-[0-9]{2} [0-2][0-9]-[0-5][0-9]-[0-5][0-9]')),
id: value(consumer(regex('[0-9a-zA-z]{8}-[0-9a-zA-z]{4}-[0-9a-zA-z]{4}-[0-9a-zA-z]{12}'))
body: "foo"
])
}

契约仓库

根据之前的配置,作为消费者,我们需要将契约放置在服务端。这就意味着,消费者需要有服务端的操作权限——但很多情况下,出于安全原因,这是不被允许的。

一个解决方法是,将所有契约集合放在同一个仓库里(Common repo with contracts)。将契约全部罗列在一起,简单清晰,也便于服务端统计消费者数目、确认接口变化会破坏的契约对应的具体消费端等。

一个契约仓库的结构可以参考:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
├── server1
│ ├── src/main/resources
│ │ └── cn.kevinz.server1.contracts
│ │ ├── client1
│ │ │ └── expectation.groovy
│ │ ├── client2
│ │ │ └── expectation.groovy
│ │ └── client3
│ │ └── expectation.groovy
│ ├── assembly.xml
│ └── pom.xml
├── server2
│ ├── src/main/resources
│ │ └── cn.kevinz.server2.contracts
│ │ ├── client1
│ │ │ └── expectation.groovy
│ │ └── client2
│ │ └── expectation.groovy
│ ├── assembly.xml
│ └── pom.xml
└── pom.xml

这里的assembly.xmlassembly插件的配置信息,用来构建jar包映射仓库中的内容。不使用它的话会报'packaging' with value 'jar' is invalid. Aggregator projects require 'pom' as packaging.错误。

要添加它,只需要在server文件夹里的pom中进行配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-assembly-plugin</artifactId>
<executions>
<execution>
<id>contracts</id>
<phase>prepare-package</phase>
<goals>
<goal>single</goal>
</goals>
<configuration>
<attach>true</attach>
<descriptors>
<descriptor>${basedir}/assembly.xml</descriptor>
</descriptors>
<!-- If you want an explicit classifier remove the following line -->
<appendAssemblyId>false</appendAssemblyId>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>1.2.1.RELEASE</version>
<extensions>true</extensions>
<configuration>
<!-- By default it would search under src/test/resources/ -->
<contractsDirectory>
${project.basedir}/src/main/resources/cn/kevinz/beerservice/contracts
</contractsDirectory>
</configuration>
</plugin>
</plugins>
</build>

assembly文件像这样:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<assembly xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.3 http://maven.apache.org/xsd/assembly-1.1.3.xsd">
<id>project</id>
<formats>
<format>jar</format>
</formats>
<includeBaseDirectory>false</includeBaseDirectory>
<fileSets>
<fileSet>
<directory>${project.basedir}/src/main/resources</directory>
<outputDirectory>/</outputDirectory>
<useDefaultExcludes>true</useDefaultExcludes>
<excludes>
<exclude>**/${project.build.directory}/**</exclude>
</excludes>
</fileSet>
</fileSets>
</assembly>

最外层的父pom除了必要的依赖包,还需要把所有server以module的形式引入:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
<modules>
<module>server1</module>
<module>server2</module>
</modules>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-dependencies</artifactId>
<version>${spring-cloud-contract.version}</version>
</dependency>
</dependencies>
</dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<version>1.2.2.RELEASE</version>
</dependency>
</dependencies>

这样,契约仓库的配置就结束了。

对于消费端,不需要修改参数,只要clone契约仓库并install一次,就可以在消费端进行测试了。

对于服务端,要配置一下pom中的contract-maven-plugin

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>1.2.1.RELEASE</version>
<extensions>true</extensions>
<configuration>
<contractsWorkOffline>true</contractsWorkOffline>
<contractDependency>
<groupId>cn.kevinz</groupId>
<artifactId>beerservice</artifactId>
<version>1.0-SNAPSHOT</version>
</contractDependency>
</configuration>
</plugin>

指定好contractDependency之后,首先install契约仓库在本地仓库生成jar包,再在服务端mvn package生成契约测试。

如果需要直接引用远端仓库文件而不打算clone到本地的话:

契约仓库:完成后运行mvn deploy部署到远端。

消费端:在@AutoConfigureStubRunner指定RepositoryRoot,并将StubsMode设定为StubRunnerProperties.StubsMode.REMOTE。例如:

1
@AutoConfigureStubRunner(repositoryRoot = "http://foo.bar", ids = "cn.kevinz:beerservice:+:stubs:8080", stubsMode = StubRunnerProperties.StubsMode.REMOTE)

服务端:需要在contract-maven-plugin插件中另外配置contractsRepositoryUrlcontractsRepositoryUsernamecontractsRepositoryPassword等,并将contractsWorkOffline去掉或设定为false

全部可配置信息请参考官方文档

CAUTION!

如果服务端的本地repo(一般是User/your_user/.m2/repository)中有不是从远端下载的契约jar包的话,创建契约测试时会报“The artifact was found in the local repository but you have explicitly
stated that it should be downloaded from a remote one”的错误。这是只需要删掉本地repo中的对应文件即可,如果想要跳过这个判定,可以参考这里的解决方案。

基于message的契约

如果对于REST不满足的话,Spring Cloud Contract也贴心地提供了基于Message的工作流程。毕竟Message大火坑是你自己要跳的

支持Spring IntegrationSpring Cloud StreamApache CamelSpring AMQP,下文以Spring AMQP为例。

在配置方面没有什么太大的区别(以上各种Message插件的配置不在本文的讨论范围之内),首要的差异体现在契约文件上。

Message契约的内容

基于Message的契约要考虑不同的情况。格式如下:

没有Input Message

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def contractDsl = Contract.make {
label 'some_label'
input {
triggeredBy('bookReturnedTriggered()')
}
outputMessage {
sentTo('activemq:output')
body('''{ "bookName" : "foo" }''')
headers {
header('BOOK-NAME', 'foo')
messagingContentType(applicationJson())
}
}
}

如果使用JUnit,则会自动生成像这样的测试类:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
// 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");
...

由Input Message触发Output

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
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')
}
}
}

会生成这样的Junit测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
...
// 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");
...

没有Output Message

1
2
3
4
5
6
7
8
9
10
11
12
13
def contractDsl = Contract.make {
label 'some_label'
input {
messageFrom('jms:delete')
messageBody([
bookName: 'foo'
])
messageHeaders {
header('sample', 'header')
}
assertThat('bookWasDeleted()')
}
}

会生成这样的JUnit测试:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
...
// given:
ContractVerifierMessage inputMessage = contractVerifierMessaging.create(
"{\\"bookName\\":\\"foo\\"}"
, headers()
.header("sample", "header"));
// when:
contractVerifierMessaging.send(inputMessage, "jms:delete");
// then:
bookWasDeleted();
...

基类

在实现整个契约测试的过程中,个人觉得最麻烦的部分就是基类了,大概还是我技艺不精的缘故。

基类是用以实现逻辑的抽象类。我们可以为每个契约都准备一个基类,也可以使用同一个类作为所有契约的基类。这里以一个没有Input的Message工作流程来举例。

在契约的input部分,我们可以看到

1
triggeredBy('bookReturnedTriggered()')

则我们需要在这份契约对应的基类中,完成这个触发器的逻辑。

如上文中所述,在Message稍微复杂的情况下,常常使用将本地json和契约对比的方式来判断契约是否被遵守。因此在触发函数中,我们只需读入json文件:

1
2
3
4
protected Object loadMessage(String file) throws IOException {
return objectMapper.readValue(getClass().getResourceAsStream(file), Object.class);
}
Object message = loadMessage(MESSAGE_JSON_PATH);

再调用

1
amqpTemplate.convertAndSend("exchange_name", "routing_key", message);

其中amqpTemplate通过@Autowired引入即可。

CAUTION!

这里第三个参数疑似只能使用Object类型,不能转换成特定类型的实例,不然会报“Failed to convert Message content”或“Can not deserialize instance”错误。

如果有在创建时需要执行的逻辑(如生成实例等),可以写在

1
2
3
4
@Before
public void setUp() {
// ...
}

中。最后,为抽象类添加标签:

1
2
3
4
@RunWith(SpringRunner.class)
@SpringBootTest(classes = QuestionnaireApplication.class, webEnvironment = SpringBootTest.WebEnvironment.MOCK,
properties = {"stubrunner.stream.enabled=false", "stubrunner.integration.enabled=false", "stubrunner.amqp.enabled=true"})
@AutoConfigureMessageVerifier

如果不打@AutoConfigureMessageVerifier标签的话,verifier是不会运行的。