领先一步
VMware 提供培训和认证,以加速您的进步。
了解更多本文简要回顾了契约测试的概念、Spring Cloud Contract 的实现方式,以及如何在多语言环境中使用 Spring Cloud Contract。
为了提高系统行为的确定性,我们编写了不同类型的测试。根据测试金字塔,主要的测试类型包括单元测试、集成测试和 UI 测试。测试越复杂,所需的时间和精力就越多,测试也越容易变得脆弱。
在分布式系统中,最常见的问题之一是测试应用程序之间的集成。假设您的服务向另一个应用程序发送 REST 请求。使用 Spring Boot 时,您可以编写一个 @SpringBootTest
来测试此行为。您可以设置 Spring 上下文,准备要发送的请求……然后将其发送到哪里?您没有启动另一个应用程序,因此会收到 Connection Refused
异常。您可以尝试模拟实际的 HTTP 调用并返回虚假的响应。但是,如果您这样做,则不会测试任何真实的 HTTP 集成、序列化和反序列化机制等。您还可以启动一个假的 HTTP 服务器(例如 WireMock)并模拟其行为。这里的问题是,作为 API 的客户端,您定义了服务器的行为。换句话说,如果您告诉假服务器在向端点 /myEndpoint
发送请求时返回文本 testText
,它就会这样做,即使真实的服务器没有此类端点。简而言之,问题在于存根可能不可靠。
另一个问题是与第三方系统的集成。可能存在一个共享实例,由于高负载,每 5 分钟就会崩溃一次。在这种情况下,我们希望将该系统屏蔽掉,使其不影响我们的集成测试,但我们需要这些存根是可靠的。
总是很诱人地设置一个端到端测试环境,生成所有应用程序,并通过整个系统运行测试。通常,这是一个很好的解决方案,可以增强您对业务功能仍然正常工作的信心。但是,端到端测试的问题在于,它们经常会无缘无故地失败,而且速度非常慢。没有什么比看到在运行了十个小时后,端到端测试由于 API 调用中的拼写错误而失败更令人沮丧的了。
解决此问题的潜在方案是契约测试。在我们详细介绍什么是契约测试之前,让我们先定义一些术语
生产方:服务器端所有者(例如,HTTP API 的所有者)或通过队列(如 RabbitMQ)发送消息的生产方。
消费方:使用 HTTP API 或侦听通过(例如)RabbitMQ 收到的消息的应用程序。
契约:生产方和消费方之间关于通信方式的协议。它不是模式。更像是使用场景。例如,对于此特定场景,我期望指定的输入,然后我用指定的输出进行回复。
契约测试:验证生产方和消费方是否可以相互集成的测试。这并不意味着功能正常工作。这种区别很重要,因为您不希望通过为每个功能编写契约来重复您的工作。契约测试断言生产方和消费方之间的集成满足契约中定义的要求。它们的主要优点是速度快且可靠。
以下示例显示了用 YAML 编写的契约
request: # (1)
method: PUT # (2)
url: /fraudcheck # (3)
body: # (4)
"client.id": 1234567890
loanAmount: 99999
headers: # (5)
Content-Type: application/json
matchers:
body:
- path: $.['client.id'] # (6)
type: by_regex
value: "[0-9]{10}"
response: # (7)
status: 200 # (8)
body: # (9)
fraudCheckStatus: "FRAUD"
"rejection.reason": "Amount too high"
headers: # (10)
Content-Type: application/json;charset=UTF-8
#From the Consumer perspective, when running a request in the integration test, we can interpret that test as follows:
#
#(1) - If the consumer sends a request
#(2) - With the "PUT" method
#(3) - to the URL "/fraudcheck"
#(4) - with the JSON body that
# * has a `client.id` field
# * has a `loanAmount` field that is equal to `99999`
#(5) - with `Content-Type` header equal to `application/json`
#(6) - and a `client.id` json entry matches a regular expression of `[0-9]{10}`
#(7) - then the response is sent with
#(8) - status equal to `200`
#(9) - and JSON body equal to
# { "fraudCheckStatus": "FRAUD", "rejection.reason": "Amount too high" }
#(10) - with header `Content-Type` equal to `application/json`
#
#From the Producer perspective, in the autogenerated producer-side test, we can interpret that test as follows:
#
#(1) - A request is sent to the producer
#(2) - With the "PUT" method
#(3) - to the URL "/fraudcheck"
#(4) - with the JSON body that
# * has a `client.id` field with a value of `1234567890`
# * has a `loanAmount` field with a value of `99999`
#(5) - with a `Content-Type` header equal to `application/json`
#(7) - then the test asserts if the response has been sent with
#(8) - status equal `200`
#(9) - and a JSON body equal to
# { "fraudCheckStatus": "FRAUD", "rejection.reason": "Amount too high" }
#(10) - with a `Content-Type` header equal to `application/json;charset=UTF-8`
本文重点介绍两种主要的契约测试类型:生产方契约测试和消费方驱动契约测试。它们之间的主要区别在于生产方和消费方的协作方式。
在生产方契约测试方法中,生产方定义契约并编写契约测试,描述 API,并在没有与其客户端进行任何协作的情况下发布存根。通常,当 API 是公开的并且 API 的所有者甚至不知道究竟是谁在使用它时,就会发生这种情况。一个例子是 Spring Initializr,它使用 Spring Rest Docs 测试发布其存根。版本 0.5.0.BUILD-SNAPSHOT
的存根可从 此处获得,带有 stubs
分类器。
在消费方驱动契约测试方法中,契约由消费方在与生产方的密切合作下提出。生产方确切地知道哪个消费方定义了哪个契约,以及当契约兼容性被破坏时哪个契约会被破坏。这种方法在处理内部 API 时更为常见。
在这两种情况下,契约都可以在生产方的存储库中定义(使用 DSL 或通过编写契约测试定义),或者存储所有契约的外部存储库中。
由于现在更容易为非 JVM 项目使用 Spring Cloud Contract,因此有必要解释打包默认值背后的基本术语并介绍 Maven 命名规范。
提示
Apache Maven 是一种软件项目管理和理解工具。基于项目对象模型 (POM) 的概念,Maven 可以从一个中心信息管理项目的构建、报告和文档。(参见 https://maven.apache.org/)
(以下定义的部分内容取自 Maven 词汇表。)
项目
:Maven 以项目的角度思考。您构建的所有内容都是项目。这些项目遵循定义良好的“项目对象模型”。项目可以依赖于其他项目,在这种情况下,后者称为“依赖项”。一个项目可能由多个子项目组成。但是,这些子项目仍然被视为项目。
构件
:构件是项目生产或使用的某种东西。Maven 为项目生成的构件示例包括 JAR、源代码和二进制发行版。每个构件都由一个组 ID 和一个构件 ID 唯一标识,在组内是唯一的。
JAR
:JAR 代表 Java ARchive。它是一种基于 ZIP 文件格式的格式。Spring Cloud Contract 将契约和生成的存根打包到 JAR 文件中。
GroupId
:组 ID 是项目的通用唯一标识符。虽然这通常是项目名称(例如,commons-collections
),但使用完全限定的包名来区分它与其他具有相似名称的项目(例如,org.apache.maven
)非常有用。通常,当发布到构件管理器时,GroupId
会被斜杠分隔,并构成 URL 的一部分。例如,对于组 ID 为 com.example
,构件 ID 为 application
的构件将为 /com/example/application/
。
分类器
:Maven 依赖项表示法如下所示:groupId:artifactId:version:classifier
。分类器是传递给依赖项的其他后缀(例如 stubs
或 sources
)。相同的依赖项(例如,com.example:application
)可以生成多个构件,这些构件彼此之间通过分类器有所不同。
构件管理器
:当您生成二进制文件、源代码或包时,您希望它们可供其他人下载、引用或重用。在 JVM 世界中,这些构件将是 JAR。对于 Ruby,它们将是 gem。对于 Docker,它们将是 Docker 镜像。您可以将这些构件存储在管理器中。此类管理器的示例包括 Artifactory 和 Nexus。
Spring Cloud Contract 是一个包含帮助用户实现各种契约测试的解决方案的伞形项目。它有两个主要模块:Spring Cloud Contract Verifier
,主要由生产方使用,以及 Spring Cloud Contract Stub Runner
,由消费方使用。
该项目允许您使用以下方式定义契约:
假设我们决定使用 YAML 编写契约。在生产方,从契约中
使用 Maven 或 Gradle 插件生成测试,以断言契约得到满足。
为其他项目生成存根以供重用。
对于使用 YAML 契约的 JVM 应用程序,使用 Spring Cloud Contract 的生产方契约方法的简化流程如下所示。
生产方
应用 Maven 或 Gradle Spring Cloud Contract 插件。
在 src/test/resources/contracts/
下定义 YAML 契约。
从契约生成测试和存根。
创建一个扩展生成测试并设置测试上下文的基类。
测试通过后,创建一个带有 stubs
分类器的 JAR,其中存储契约和存根。
将带有 stubs
分类器的 JAR 上传到二进制存储。
消费方
使用 Stub Runner 获取生产者的存根。Stub Runner 启动内存中的 HTTP 服务器(默认情况下,这些是 WireMock 服务器),并使用存根填充它们。
针对存根运行测试。
因此,使用 Spring Cloud Contract 和契约测试可以获得
存根的可靠性:只有在测试通过后才会生成它们。
存根的可重用性:多个消费者可以下载和重用它们。
分布式系统由用不同语言和框架编写的应用程序构成。Spring Cloud Contract 的“问题”之一是 DSL 必须用 Groovy 编写。即使契约不需要任何特殊的语言知识,对于非 JVM 用户来说,它也成了一种问题。
在生产者端,Spring Cloud Contract 生成 Java 或 Groovy 中的测试。当然,在非 JVM 环境中使用这些测试也成了一种问题。您不仅需要安装 Java,而且测试是使用 Maven 或 Gradle 插件生成的,这需要使用这些构建工具。
从 Edgware.SR2
发布列车和 1.2.3.RELEASE
的 Spring Cloud Contract 开始,我们决定添加一些功能,以便在非 JVM 世界中更广泛地采用 Spring Cloud Contract。
我们添加了对使用 YAML 编写契约的支持。YAML 是一种(又一种)标记语言,它不依赖于任何特定的语言,并且已经被广泛使用。这应该可以解决使用与任何特定语言相关的 DSL 定义契约的“问题”。
为了隐藏实现细节(例如 Java 测试的生成、插件设置或 Java 安装),我们需要引入一个抽象层。我们决定通过使用 Docker 镜像来隐藏这些细节。我们将所有项目设置、所需包和文件夹结构封装到 Docker 镜像中,这样用户就不需要任何知识,除了所需的环境变量。
我们为 生产者 和 消费者 引入了 Docker 镜像。所有与 JVM 相关的逻辑都封装在 Docker 容器中,这意味着您无需安装 Java 即可生成测试并使用 Stub Runner 运行存根。
以下部分将逐步介绍一个 NodeJS 应用程序使用 Spring Cloud Contract 进行测试的示例。代码是从 https://github.com/bradtraversy/bookstore 分支出来的,并在 https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs 下可用。我们的目标是尽可能快地、以最少的努力开始为现有应用程序生成测试和存根。
让我们克隆简单的 NodeJS MVC 应用程序,如下所示
$ git clone https://github.com/spring-cloud-samples/spring-cloud-contract-nodejs
$ cd spring-cloud-contract-nodejs
它连接到 Mongo DB 数据库以存储有关书籍的数据。
YAML 契约位于 /contracts
文件夹下,如下所示
$ ls contracts
1_shouldAddABook.yml 2_shouldReturnListOfBooks.yml
数字后缀告诉 Spring Cloud Contract 从这些契约生成的测试需要按顺序执行。存根是有状态的,这意味着只有在执行了与 1_shouldAddABook
匹配的请求后,2_shouldReturnListOfBooks.yml
才能从存根 HTTP 服务器获得。
重要
在实际示例中,我们将以契约测试模式运行 NodeJS 应用程序,其中对数据库的调用将被存根化,并且不需要有状态存根。在本例中,我们希望展示如何立即从 Spring Cloud Contract 中获益。
让我们看一下其中一个存根
description: |
Should add a book
request:
method: POST
url: /api/books
headers:
Content-Type: application/json
body: '{
"title" : "Title",
"genre" : "Genre",
"description" : "Description",
"author" : "Author",
"publisher" : "Publisher",
"pages" : 100,
"image_url" : "https://d213dhlpdb53mu.cloudfront.net/assets/pivotal-square-logo-41418bd391196c3022f3cd9f3959b3f6d7764c47873d858583384e759c7db435.svg",
"buy_url" : "https://pivotal.io"
}'
response:
status: 200
契约规定,如果向 /api/books
发送 POST
请求,并带有 Content-Type: application/json
的标头和上述主体,则响应应为 200
。现在,在运行契约测试之前,让我们分析 Spring Cloud Contract Docker 镜像的要求。
该镜像在 DockerHub 的 SpringCloud 组织下可用。
一旦您挂载契约并传递环境变量,该镜像将
生成契约测试。
针对提供的 URL 执行测试。
生成 WireMock 存根。
将存根发布到工件管理器。(此步骤是可选的,但默认情况下已启用。)
重要
生成的测试假设您的应用程序正在运行并准备好在指定的端口上监听请求。这意味着您必须在运行契约测试之前运行它。
Docker 镜像在 /contracts
文件夹下搜索契约。运行测试的输出位于 /spring-cloud-contract/build
文件夹下(它对调试很有用)。在运行构建时,您需要挂载这些卷。
Docker 镜像还需要一些环境变量,这些变量指向您的正在运行的应用程序、工件管理器实例等,如下所列
PROJECT_GROUP
:项目的组 ID。默认为 com.example
。
PROJECT_VERSION
:项目的版本。默认为 0.0.1-SNAPSHOT
。
PROJECT_NAME
. 工件 ID。默认为 example
。
REPO_WITH_BINARIES_URL
- 工件管理器的 URL。默认为 [https://127.0.0.1:8081/artifactory/libs-release-local](https://127.0.0.1:8081/artifactory/libs-release-local)
,这是在本地运行时 Artifactory 的默认 URL。
REPO_WITH_BINARIES_USERNAME
:(可选)工件管理器受保护时的用户名。
REPO_WITH_BINARIES_PASSWORD
:(可选)工件管理器受保护时的密码。
PUBLISH_ARTIFACTS
:如果设置为 true
,则将工件发布到二进制存储。默认为 true
。
以下环境变量用于测试运行时
APPLICATION_BASE_URL
:应执行测试的 URL。请记住,它必须能够从 Docker 容器访问(localhost
不起作用)。
APPLICATION_USERNAME
:(可选)应用程序的基本身份验证用户名。
APPLICATION_PASSWORD
:(可选)应用程序的基本身份验证密码。
重要
要运行此示例,您需要安装 Docker、Docker Compose 和 npm。
由于我们要运行测试,因此可以使用
$ npm install
$ npm test
但是,出于学习目的,让我们将其拆分为几部分,如下所示(我们将分析 bash 脚本的每一行)
# Install the required npm packages
$ npm install
# Stop docker infra (mongodb, artifactory)
$ ./stop_infra.sh
# Start docker infra (mongodb, artifactory)
$ ./setup_infra.sh
# Kill & Run app
$ pkill -f "node app"
$ nohup node app &
# Prepare environment variables
$ export SC_CONTRACT_DOCKER_VERSION="1.2.3.RELEASE"
$ export APP_IP="192.168.0.100" # This has to be the IP that is available outside of Docker container
$ export APP_PORT="3000"
$ export ARTIFACTORY_PORT="8081"
$ export APPLICATION_BASE_URL="http://${APP_IP}:${APP_PORT}"
$ export ARTIFACTORY_URL="http://${APP_IP}:${ARTIFACTORY_PORT}/artifactory/libs-release-local"
$ export CURRENT_DIR="$( pwd )"
$ export PROJECT_NAME="bookstore"
$ export PROJECT_GROUP="com.example"
$ export PROJECT_VERSION="0.0.1.RELEASE"
# Execute contract tests
$ docker run --rm -e "APPLICATION_BASE_URL=${APPLICATION_BASE_URL}" \
-e "PUBLISH_ARTIFACTS=true" -e "PROJECT_NAME=${PROJECT_NAME}" \
-e "PROJECT_GROUP=${PROJECT_GROUP}" -e "REPO_WITH_BINARIES_URL=${ARTIFACTORY_URL}" \
-e "PROJECT_VERSION=${PROJECT_VERSION}" -v "${CURRENT_DIR}/contracts/:/contracts:ro" \
-v "${CURRENT_DIR}/node_modules/spring-cloud-contract/output:/spring-cloud-contract-output/" \
springcloud/spring-cloud-contract:"${SC_CONTRACT_DOCKER_VERSION}"
# Kill app
$ pkill -f "node app"
将会发生的是,通过 bash 脚本
基础设施(MongoDb、Artifactory)将被设置。
由于 NodeJS 应用程序中没有模拟数据库的限制,因此契约也表示了有状态的情况。
第一个请求是 POST
,它会导致数据插入到数据库中。
第二个请求是 GET
,它返回一个包含一个先前插入元素的数据列表。
NodeJS 应用程序已启动(在端口 3000
上),可在 192.168.0.100
访问。
契约测试由 Docker 生成,并且测试针对正在运行的应用程序执行。
契约取自 /contracts
文件夹。
测试执行的输出可在 node_modules/spring-cloud-contract/output
中找到。
存根已上传到 Artifactory。您可以在 https://127.0.0.1:8081/artifactory/libs-release-local/com/example/bookstore/0.0.1.RELEASE/ 查看它们。存根位于 https://127.0.0.1:8081/artifactory/libs-release-local/com/example/bookstore/0.0.1.RELEASE/bookstore-0.0.1.RELEASE-stubs.jar。
总之,我们定义了 YAML 契约,运行了 NodeJS 应用程序,并运行了 Docker 镜像以生成契约测试和存根并将它们上传到 Artifactory。
在本例中,我们发布了一个 spring-cloud/spring-cloud-contract-stub-runner Docker 镜像,该镜像启动 Stub Runner 的独立版本。
提示
如果您习惯于运行 java -jar
命令而不是运行 Docker,您可以从 Maven 下载独立 JAR(例如,版本 1.2.3.RELEASE),如下所示:wget -O stub-runner.jar 'https://search.maven.org/remote_content?g=org.springframework.cloud&a=spring-cloud-contract-stub-runner-boot&v=1.2.3.RELEASE'
您可以将任何 属性 作为环境变量传递。约定是所有字母都应大写,并且单词分隔符和点(.
)应替换为下划线(_
)。例如,stubrunner.repositoryRoot
属性应表示为 STUBRUNNER_REPOSITORY_ROOT
环境变量。
假设我们希望在端口 9876
上运行 bookstore 应用程序的存根。为此,让我们使用存根运行 Stub Runner Boot 应用程序,如下所示
# Provide the Spring Cloud Contract Docker version
$ export SC_CONTRACT_DOCKER_VERSION="1.2.3.RELEASE"
# The IP at which the app is running and the Docker container can reach it
$ export APP_IP="192.168.0.100"
# Spring Cloud Contract Stub Runner properties
$ export STUBRUNNER_PORT="8083"
# Stub coordinates 'groupId:artifactId:version:classifier:port'
$ export STUBRUNNER_IDS="com.example:bookstore:0.0.1.RELEASE:stubs:9876"
$ export STUBRUNNER_REPOSITORY_ROOT="http://${APP_IP}:8081/artifactory/libs-release-local"
# Run the docker with Stub Runner Boot
$ docker run --rm -e "STUBRUNNER_IDS=${STUBRUNNER_IDS}" \
-e "STUBRUNNER_REPOSITORY_ROOT=${STUBRUNNER_REPOSITORY_ROOT}" \
-p "${STUBRUNNER_PORT}:${STUBRUNNER_PORT}" -p "9876:9876" \
springcloud/spring-cloud-contract-stub-runner:"${SC_CONTRACT_DOCKER_VERSION}"
该脚本
启动一个独立的 Spring Cloud Contract Stub Runner 应用程序。
使 Stub Runner 下载具有以下坐标的存根:com.example:bookstore:0.0.1.RELEASE:stubs
。
从 Artifactory 中的 [http://192.168.0.100:8081/artifactory/libs-release-local](http://192.168.0.100:8081/artifactory/libs-release-local)
下载存根。
在(延迟后)端口 8083
上启动 Stub Runner。
在端口 9876
上运行存根。
在服务器端,我们构建了一个有状态的存根。让我们使用 curl 来断言存根已正确设置,如下所示
# let's execute the first request (no response is returned)
$ curl -H "Content-Type:application/json" -X POST \
--data '{ "title" : "Title", "genre" : "Genre", "description" : "Description", "author" : "Author", "publisher" : "Publisher", "pages" : 100, "image_url" : "https://d213dhlpdb53mu.cloudfront.net/assets/pivotal-square-logo-41418bd391196c3022f3cd9f3959b3f6d7764c47873d858583384e759c7db435.svg", "buy_url" : "https://pivotal.io" }' https://127.0.0.1:9876/api/books
# Now it's time for the second request
$ curl -X GET https://127.0.0.1:9876/api/books
# You should receive the contents of the JSON
总之,一旦存根上传,您就可以运行一个带有几个环境变量的 Docker 镜像,并在您的集成测试中重用它们,而不管使用的编程语言是什么。
在这篇博文中,我们解释了契约测试是什么以及为什么它们很重要。我们介绍了如何使用 Spring Cloud Contract 生成和执行契约测试。最后,我们介绍了一个如何为非 JVM 应用程序使用 Spring Cloud Contract Docker 镜像(用于生产者和消费者)的示例。
阅读 Spring Cloud Contract 的文档。
查看 Bookstore 示例。