基于契约,对消费者与生产者间的协作的验证, 本质上就是验证生产者所提供的内容是否满足消费者的期望。
契约测试在行业内,主要分为两种类型,消费者驱动的契约测试和生产者驱动的契约测试,最常见的就是消费者驱动的契约测试,简称 CDC(Consumer Driven Contract Test);根据消费者驱动契约 ,我们可以将服务分为消费者端和生产者端,而消费者驱动的契约测试的核心思想在于是从消费者业务实现的角度出发,由消费者自己会定义需要的数据格式以及交互细节,并驱动生成一份契约文件。然后生产者根据契约文件来实现自己的逻辑,并在持续集成环境中持续验证。
契约测试帮我们解决的问题:
TDD是测试驱动开发(Test-Driven Development)的英文简称,是敏捷开发中的一项核心实践和技术,也是一种设计方法论。TDD的原理是在开发功能代码之前,先编写单元测试用例代码,测试代码确定需要编写什么产品代码。TDD虽是敏捷方法的核心实践,但不只适用于XP(Extreme Programming),同样可以适用于其他开发方法和过程。
消费者驱动契约
cdc核心原则:
使用来自另一个应用程序的功能或数据来完成其工作的流程.对于使用HTTP的应用程序,消费者始终是发起HTTP请求的应用程序(例如Web前端),而已数据流的方向无关,对于使用队列的应用程序,消费者是从队列中读取消息的应用程序
通常通过 API 提供功能或数据供其他应用程序使用的应用程序(通常称为服务) 。对于使用 HTTP 的应用程序,提供者是返回响应的应用程序。对于使用队列的应用程序,提供者(也称为生产者)是将消息写入队列的应用程序。
契约是一份协议,它规定了API 或消息通信应该是怎么样的,每一份契约是一份交互的集合,每个交互描述:
Spring Cloud Contract是一个包含解决方案的总括项目,可帮助用户实现不同类型的契约测试。它带有两个主要模块:Spring Cloud Contract Verifier
主要由生产者方使用,以及Spring Cloud Contract Stub Runner
由消费者方使用。
Spring Cloud Contract 中各部分的关系:
使用 Spring Cloud Contract 和 契约测试 可以提供:
默认情况下,Spring Cloud Contract 与Wiremock集成为 HTTP 服务器存根。
注意: Spring Cloud Contract 的目的不是开始在契约中编写业务功能, 契约测试用于测试应用程序之间的契约,而不是模拟完整的行为。 假设我们有一个欺诈检查的业务用例,如果一个用户可能因为 100 个不同的原因而成为欺诈,你可以去将创建两份契约,一份契约的内容定义为欺诈,另一份契约定义为未欺诈,不需要定义100多种契约.
设计者Marcin Grzejszczak也在stack flow中提到:
Spring Cloud Contract 支持使用以下语言编写的 DSL:
要在 Java 中编写合约定义,您需要创建一个实现Supplier
接口(对于单个合约)或Supplier
(对于多个合约)的类
支持动态配置属性,支持正则表达式,
以下是Groovy格式的sample:
/**
* @author shil
* @date 2022/5/25
*/
Contract.make {
description("should return all customers")
request {
url("/customers")
method GET()
}
response {
status 200
headers {
header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
}
body([[id: 1L, name: "shiliang"], [id: 2L, name: "Tom"]])
}
}
testImplementation("org.springframework.cloud:spring-cloud-starter-contract-verifier")
/**
* @author shil
* @date 2022/5/25
*/
@SpringBootTest(classes = ProviderServiceDemoApplication.class)
public class BaseTestClass {
@Autowired
private CustomerController webApplicationContext;
@BeforeEach
public void setup() {
RestAssuredMockMvc.standaloneSetup(webApplicationContext);
}
}
contracts {
testFramework = "JUNIT5"
packageWithBaseClasses = 'kl.v2x.providerservicedemo'
baseClassMappings {
baseClassMapping(".*.*", "kl.v2x.providerservicedemo.BaseTestClass")
}
contractsDslDir = new File(project.rootDir, "src/test/java/contracts")
}
$rootDir/src/test/resources/contracts
,编写完成后运行构建,将会生成测试类、stub,生成的测试类可以验证服务本身是否符合契约,默认情况下,测试类在 org.springframework.cloud.contract.verifier.tests 下生成;下面是生成的测试类sample(默认使用的是MockMvc测试框架):
public class ContractVerifierTest extends BaseTestClass {
@Test
public void validate_shouldReturnAllCustomers() throws Exception {
// given:
MockMvcRequestSpecification request = given();
// when:
ResponseOptions response = given().spec(request)
.get("/customers");
// then:
assertThat(response.statusCode()).isEqualTo(200);
assertThat(response.header("Content-Type")).isEqualTo("application/json");
// and:
DocumentContext parsedJson = JsonPath.parse(response.getBody().asString());
assertThatJson(parsedJson).array().contains("['id']").isEqualTo(1L);
assertThatJson(parsedJson).array().contains("['name']").isEqualTo("shiliang");
assertThatJson(parsedJson).array().contains("['id']").isEqualTo(2L);
assertThatJson(parsedJson).array().contains("['name']").isEqualTo("Tom");
}
}
消费者端主要是通过使用Spring Cloud Contract Stub Runner去获取一个运行的WireMock实例,这个实例是模拟被消费的服务
使用 Stub Runner 获取生产者的存根,Stub Runner 在内存 中启动一个HTTP 服务器(默认情况下,都是WireMock 服务器),消费者针对存根进行测试
testImplementation 'org.springframework.cloud:spring-cloud-starter-contract-stub-runner'
测试类通过添加@AutoConfigureStubRunner注解,并提供group-id和artifact-id,将其配置到注解属性中,配置的属性主要是从什么地方fetch stub.
@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment=WebEnvironment.NONE)
@AutoConfigureStubRunner(ids = {"com.example:http-server-dsl:+:stubs:6565"},
stubsMode = StubRunnerProperties.StubsMode.LOCAL)
public class LoanApplicationServiceTests {
. . .// test
}
开始测试,测试过程会去拉取stub,并在本地启动,充当服务提供者供消费者消费,就可以发起请求,获取相应,判断相应是否与预期的契约一致
在生产者契约测试方法中,,生产者去定义契约然后编写契约测试,描述API并发布存根,无需与其客户端合作.通常这种情况发生在当API是公开的并且API的所有者甚至不知道到底是谁在使用它.
在消费者驱动的契约测试方法中,契约由消费者建议,与生产者密切合作。生产者确切地知道哪个消费者定义了哪个契约以及当契约兼容性被破坏时哪个契约被破坏。这种方法在使用内部 API 时更为常见。
在这两种类型情况下,契约都可以在生产者的存储库中定义(使用 DSL 或通过编写契约测试来定义)或存储所有契约的外部存储库。
契约测试的精髓就是消费者驱动,而PACT的设计理念就是遵循和实现消费者驱动测试.Pact是一款编码优先的工具,使用契约测试来测试HTTP和消息集成,契约测试断言应用程序之间是否符合共同的约定(记录在契约中),如果没有契约测试,唯一的方法去确保应用程序间能够正确交互的方法是通过昂贵的且脆弱的集成测试
对于 HTTP
消费者项目中的 Pact 测试使用 Pact mock provider来模拟实际的服务提供者,这意味着可以运行类似集成的测试,而无需实际的服务提供者可用;
对于消息
消费者和提供者通常使用诸如队列、主题或事件总线之类的中介来交换消息,但是,Pact message是故意与这些技术无关,它只关注有效负载,因此没有 Pact 模拟队列/主题/总线之类的东西。
请求和响应相对。每个交互都有一个描述和一个或多个提供者状态。HTTP 协议由一系列交互组成。
包含在消费者测试中定义的 JSON 序列化交互(请求和响应)或消息的文件。这是契约。一个契约定义:
Pact 规范是一个管理实际生成的 Pact 文件的结构的文档,以允许语言之间的互操作性,使用语义版本控制来指示重大更改。
Pact Broker 是用于共享 Pact 合约和验证结果的应用程序,它也是一个永久运行的外部托管服务,具有 API 和 UI,允许将契约测试集成到 CI/CD 管道中。与 Pact 客户端一样,Pact Broker 是一个开源项目。
对于 HTTP
为了验证Pact 契约,包含在一个pact 文件中的请求将针对提供程序代码进行重放,并检查返回的响应以确保它们与 pact 文件中的预期相匹配.
对于消息
执行提供程序上的一段代码以生成给定描述的消息,并检查生成的消息以确保它与 pact 文件中的预期匹配。
在消费者方面,提供者状态是一个名称,描述了提供者在针对它重放给定请求时应该处于的“状态” ——例如“当用户 John Doe 存在时”或“当用户 John Doe 拥有银行账户”。这些允许在不同的场景下测试相同的端点。
在提供者端,当执行协议验证时,提供者状态名称将用于标识在请求执行之前应该运行的设置代码块。提供者状态设置代码由提供者团队编写。
每一个交互都通过Pact框架进行测试,由消费者代码库中的单元测试框架进行驱动:
如下图:
在消费者测试期间,对 Pact 模拟提供者的每个请求都记录到契约文件中,以及它的预期响应。
只有在每个步骤都完成且没有错误的情况下,Pact测试才会成功,通常,交互定义和消费者测试是一起编写的
在Pact中每个交互被认为是独立的,这意味着每个测试只测试一个交互.如果你需要描述依赖于预先存在状态的交互,可以使用provider states去完成. Provider states允许你描述生成预期响应所需的提供者的先决条件.
举例:与其编写一个测试用例含义为“创建一个用户123,然后登陆”,不如编写两个单独的交互,一个是“创建用户 123”,另一个是定义提供者状态为“用户 123 存在”,即“以用户身份登录” 123”。
与消费者测试相比,提供者验证是完全通过Pact框架来驱动进行的
在提供者验证中,每个请求都发送给提供者,并将其生成的实际响应与消费者测试中描述的最小预期响应进行比较。
如果每个请求生成的响应至少包含最小预期响应中描述的数据,则提供者验证通过。
在很多用例中,提供者需要处于特定的状态(例如“用户123已经登陆”或“客户456拥有一个发票”等待),Pact框架支持你在请求交互之前设置由Provider State描述的数据来支持这一点:
如果我们将每次交互的消费者测试和提供者验证过程配对,则消费者和提供者之间的契约将得到全面测试,而无需一起启动服务。
基于契约进行合作
完成契约测试后,您需要一个流程来管理契约测试流程。这就是Pact Broker的用武之地。Pact Broker 使您能够:
✅跨团队共享和协作契约
✅跨代码分支和环境去管理契约
✅编排构建以了解何时可以安全部署
✅集成到您的流程和工具中
基于一份契约测试
测试范围相同,它们都只专注于确保请求创建和响应处理是正确的,而不是用于测试特定的、完整的业务逻辑
消费者的契约测试都是通过启动一个HTTP mock server进行测试
SCC: Artifactory/Nexus、classpath、Git 代码库或 Pact broker
PACT: classpath、Pact broker(推荐, 需要额外搭建和维护Pact Broker)
SCC: Groogy、Yaml
Pact: JSON
SCC: 消费者生产者一起定义一份契约,消费者提供建议,契约在生产方编写
Pact: 消费者去定义契约,主导方在消费者,生产者负责实现
SCC: 自己需要先编写一个测试基类,SCC帮你生成测试类,测试类去校验服务自身的响应是否与预期响应一致
PACT: 通过一个Mock Provider Server(HTTP服务)模拟消费者,往生产者自身发请求,校验响应是否与预期响应一致
Pact: 在consumer端生成契约文件,发布到Pact Broker,而后,provider从Pact Broker获取契约文件,触发provider端执行契约测试。
SCC: 实际生成契约文件的工作是发生在provider端的,基于这份契约文件,在provider端,生成了Java的测试案例,这些测试案例用于provider的功能测试;而在Consumer端,使用同一份契约文件作为Stub,生成了基于WireMock的mock service,consumer可以使用该mock service来做集成测试。
SCC: 支持JVM和非JVM语言,但是非JVM语言是通过Docker镜像实现,并没有原生支持
Spring Cloud Contract支持多语言(非JVM语言)是通过Docker镜像来帮助完成的,为了隐藏实现细节(例如生成 java 测试、插件设置或 Java 安装),SCC引入一个抽象层。通过使用Docker镜像来隐藏它们。SCC将所有项目设置、所需的包和文件夹结构封装在一个 docker 映像中,这样除了所需的环境变量之外,用户不需要任何知识。
PACT: 原生支持多种语言:java、C++、JS、Go、Python、Swift、PHP等(基于PACT规范实现)
SCC: 需要手动编写契约,也可以通过集成Spring Rest Doc框架,通过springmvc的单元测试可直接生成契约
PATC: 基于消费者代码编写生成契约
Pact作为消费者驱动契约测试的倡导者,真正地实践了消费者驱动的契约测试。相对的,SCC,既没有实际的将契约作为被测对象来进行测试,更没有确实地实现”消费者驱动”。SCC的做法,实际上是基于同一份契约,分别驱动了consumer端的集成测试和provider端的功能测试。所以,Pact和SCC的区别,就在于,前者做的是”契约测试”,后者做的是”基于契约的测试(契约驱动的测试)”。
Spring Cloud Contract: Contributors: 138、starts: 641、forks: 395
PACT-JVM: Contributors: 138、starts: 924、 forks: 438
Marcin Grzejszczak解答:
两个框架之间的主要区别在于测试开发过程的起点。SCC 先在生产者端定义契约,然后将其传递给消费者进行验证。相比之下,PACT 是消费者驱动的,这意味着消费者向提供者提供他们对合同的期望,以验证它是否符合预期的合同定义。
控制权更多的是在提供者, 提供者契约是一种方法,生产者声明如何使用 API,而不关心哪个消费者以何种方式使用它
相同点: 本质上都是在解决同一个问题
不同点:
总的来说:
特征:
用于发布和检索协议的 RESTful API。
用于导航 API 的嵌入式 API 浏览器。
每个协议的自动生成的文档。
动态生成的网络图,因此您可以可视化您的微服务网络。
显示提供者验证结果,以便您了解是否可以安全部署。
提供兼容的消费者和提供者版本的“矩阵”,以便您知道哪些版本可以安全地部署在一起。
提供徽章以在您的自述文件中显示协议验证状态。
允许标记应用程序版本(即“prod”、“feat/customer-preferences” )以允许类似存储库的工作流。
提供 webhook 以在协议更改时触发操作,例如。运行提供程序构建,通知 Slack 频道。
查看 Pact 版本之间的差异,以便您了解预期发生了哪些变化。
Docker 契约代理
用于将 Pact 工作流整合到您的持续集成过程中的CLI 。
结论: 推荐PACT
Spring Cloud Contract 官方文档
PACT官方文档
契约测试之核心解惑
Contract Test — Spring Cloud Contract vs PACT
Spring Cloud Contract 社区