mirror of
https://github.com/zhwei820/learn.lianglianglee.com.git
synced 2025-09-30 23:26:43 +08:00
1125 lines
25 KiB
HTML
1125 lines
25 KiB
HTML
<!DOCTYPE html>
|
||
|
||
<!-- saved from url=(0046)https://kaiiiz.github.io/hexo-theme-book-demo/ -->
|
||
|
||
<html xmlns="http://www.w3.org/1999/xhtml">
|
||
|
||
<head>
|
||
|
||
<head>
|
||
|
||
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
|
||
|
||
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1.0, user-scalable=no">
|
||
|
||
<link rel="icon" href="/static/favicon.png">
|
||
|
||
<title>07 契约测试:如何进行消费者驱动的契约测试?.md.html</title>
|
||
|
||
<!-- Spectre.css framework -->
|
||
|
||
<link rel="stylesheet" href="/static/index.css">
|
||
|
||
<!-- theme css & js -->
|
||
|
||
<meta name="generator" content="Hexo 4.2.0">
|
||
|
||
</head>
|
||
|
||
|
||
|
||
<body>
|
||
|
||
|
||
|
||
<div class="book-container">
|
||
|
||
<div class="book-sidebar">
|
||
|
||
<div class="book-brand">
|
||
|
||
<a href="/">
|
||
|
||
<img src="/static/favicon.png">
|
||
|
||
<span>技术文章摘抄</span>
|
||
|
||
</a>
|
||
|
||
</div>
|
||
|
||
<div class="book-menu uncollapsible">
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="/" class="current-tab">首页</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li><a href="../">上一级</a></li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
<ul class="uncollapsible">
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/00 开篇词 既往不恋,当下不杂,未来不迎.md">00 开篇词 既往不恋,当下不杂,未来不迎.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/01 微服务架构有哪些特点?.md">01 微服务架构有哪些特点?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/02 微服务架构下的质量挑战.md">02 微服务架构下的质量挑战.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/03 微服务架构下的测试策略.md">03 微服务架构下的测试策略.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/04 单元测试:怎样提升最小可测试单元的质量?.md">04 单元测试:怎样提升最小可测试单元的质量?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/05 集成测试:如何进行微服务的集成测试?.md">05 集成测试:如何进行微服务的集成测试?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/06 组件测试:如何保证单服务的质量?.md">06 组件测试:如何保证单服务的质量?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
<a class="current-tab" href="/专栏/微服务质量保障 20 讲-完/07 契约测试:如何进行消费者驱动的契约测试?.md">07 契约测试:如何进行消费者驱动的契约测试?.md.html</a>
|
||
|
||
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/08 端到端测试:站在用户视角验证整个系统.md">08 端到端测试:站在用户视角验证整个系统.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/09 微服务架构下的质量保障体系全景概览.md">09 微服务架构下的质量保障体系全景概览.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/10 流程规范篇:高速迭代的研发过程需要怎样的规范?.md">10 流程规范篇:高速迭代的研发过程需要怎样的规范?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/11 测试技术篇:测试技术这么多,我该如何选型?.md">11 测试技术篇:测试技术这么多,我该如何选型?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/12 测试技术篇:如何提升测试效率?.md">12 测试技术篇:如何提升测试效率?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/13 测试技术篇:专项测试技术解决了哪些专项问题?.md">13 测试技术篇:专项测试技术解决了哪些专项问题?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/14 CICD 篇:如何更好地利用多个“测试”环境?.md">14 CICD 篇:如何更好地利用多个“测试”环境?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/15 CICD 篇:如何构建持续交付工具链?.md">15 CICD 篇:如何构建持续交付工具链?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/16 度量与运营篇:如何做好质量和效率的度量与运营?.md">16 度量与运营篇:如何做好质量和效率的度量与运营?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/17 度量与运营篇:如何度量与运营效率和价值?.md">17 度量与运营篇:如何度量与运营效率和价值?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/18 组织保障篇:质量是设计出来的.md">18 组织保障篇:质量是设计出来的.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/19 软件测试新趋势探讨.md">19 软件测试新趋势探讨.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
<li>
|
||
|
||
|
||
|
||
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/20 结束语 QA 如何打造自身的核心竞争力?.md">20 结束语 QA 如何打造自身的核心竞争力?.md.html</a>
|
||
|
||
|
||
|
||
</li>
|
||
|
||
</ul>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<div class="sidebar-toggle" onclick="sidebar_toggle()" onmouseover="add_inner()" onmouseleave="remove_inner()">
|
||
|
||
<div class="sidebar-toggle-inner"></div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<script>
|
||
|
||
function add_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.add('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function remove_inner() {
|
||
|
||
let inner = document.querySelector('.sidebar-toggle-inner')
|
||
|
||
inner.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
function sidebar_toggle() {
|
||
|
||
let sidebar_toggle = document.querySelector('.sidebar-toggle')
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let content = document.querySelector('.off-canvas-content')
|
||
|
||
if (sidebar_toggle.classList.contains('extend')) { // show
|
||
|
||
sidebar_toggle.classList.remove('extend')
|
||
|
||
sidebar.classList.remove('hide')
|
||
|
||
content.classList.remove('extend')
|
||
|
||
} else { // hide
|
||
|
||
sidebar_toggle.classList.add('extend')
|
||
|
||
sidebar.classList.add('hide')
|
||
|
||
content.classList.add('extend')
|
||
|
||
}
|
||
|
||
}
|
||
|
||
|
||
|
||
|
||
|
||
function open_sidebar() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.add('show')
|
||
|
||
overlay.classList.add('show')
|
||
|
||
}
|
||
|
||
function hide_canvas() {
|
||
|
||
let sidebar = document.querySelector('.book-sidebar')
|
||
|
||
let overlay = document.querySelector('.off-canvas-overlay')
|
||
|
||
sidebar.classList.remove('show')
|
||
|
||
overlay.classList.remove('show')
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
<div class="off-canvas-content">
|
||
|
||
<div class="columns">
|
||
|
||
<div class="column col-12 col-lg-12">
|
||
|
||
<div class="book-navbar">
|
||
|
||
<!-- For Responsive Layout -->
|
||
|
||
<header class="navbar">
|
||
|
||
<section class="navbar-section">
|
||
|
||
<a onclick="open_sidebar()">
|
||
|
||
<i class="icon icon-menu"></i>
|
||
|
||
</a>
|
||
|
||
</section>
|
||
|
||
</header>
|
||
|
||
</div>
|
||
|
||
<div class="book-content" style="max-width: 960px; margin: 0 auto;
|
||
|
||
overflow-x: auto;
|
||
|
||
overflow-y: hidden;">
|
||
|
||
<div class="book-post">
|
||
|
||
<p id="tip" align="center"></p>
|
||
|
||
<div><h1>07 契约测试:如何进行消费者驱动的契约测试?</h1>
|
||
|
||
<p>上一课时,我讲到了微服务架构下的组件测试,它是针对单个微服务的验收测试,虽然保障了单个微服务功能的正确性,但要想保障微服务间交互功能的正确性,就需要进行契约测试。</p>
|
||
|
||
<h3>契约测试产生的背景</h3>
|
||
|
||
<p>在介绍契约测试之前,首先来看下什么是契约。现实世界中,契约是一种书面的约定,比如租房时需要跟房东签房屋租赁合同、买房时需要签署购房合同、换工作时你要跟公司签署劳动合同等。在信息世界中,契约也有很多使用场景,像 TCP/IP 协议簇、HTTP 协议等,只是这些协议已经成为一种技术标准,我们只需要按标准方式接入就可以实现特定的功能。</p>
|
||
|
||
<p>具体到业务场景中,契约是研发人员在技术设计时达成的约定,它规定了服务提供者和服务消费者的交互内容。可见,无论是物理世界还是信息世界,<strong>契约是双方或多方共识的一种约定,需要协同方共同遵守。</strong></p>
|
||
|
||
<p>在微服务架构中,服务与服务之间的交互内容更需要约定好。因为一个微服务可能与其他 N 个微服务进行交互,只有对交互内容达成共识并保持功能实现上的协同,才能实现业务功能。我们来看一个极简场景,比如我们要测试服务 A 的功能,然而需要服务 A 调用服务 B 才能完成,如图:</p>
|
||
|
||
<p><img src="assets/CgqCHl8rwdWARQ7JAAAlzqKNM8A650.png" alt="Drawing 0.png" /></p>
|
||
|
||
<p>服务 A 所属的研发测试团队在测试时,太难保证服务 B 是足够稳定的,而服务 B 的不稳定会导致测试服务 A 时效率下降、测试稳定性降低。因为,当服务 B 有阻塞性的缺陷或者干脆宕机时,你需要判断是环境问题还是功能缺陷导致的,这些情况在微服务的测试过程中属于常见的痛点问题。因此,为了提升测试效率和测试稳定性,我们会通过<strong>服务虚拟化技术</strong>来模拟外部服务,如图:</p>
|
||
|
||
<p><img src="assets/CgqCHl8rwd2AHsPJAAAqXjJCb3o139.png" alt="Drawing 2.png" /></p>
|
||
|
||
<p>需要特别注意的是,如果此时你针对内部系统的测试用例都执行通过了,可以说明你针对服务 A的测试是通过的吗?答案是否定的。因为这里面有个<strong>特别重要的假设是</strong>,服务虚拟化出来的Mock B 服务与真实的 B 服务是相等的。而事实是,它们可能只在你最初进行服务虚拟化时是相等的,随着时间的推移,它们很难保持相等。</p>
|
||
|
||
<p><img src="assets/CgqCHl8rweeAaDkdAABVWLFzSS8274.png" alt="Drawing 4.png" /></p>
|
||
|
||
<p>可能你会说,保持相等不就是个信息同步的工作嘛,有那么难吗?事实上,说起来容易做起来真的挺难:在实际的研发场景下,一个研发团队需要维护若干(a)个服务,每个服务又有数十(b)个接口,每个接口又被多(c)个团队的服务所调用,可见信息同步的工作量是巨大的(a<em>b</em>c)。</p>
|
||
|
||
<p>所以在微服务团队中,如下情况极为常见,每一项都会导致信息不同步:服务 B 的开发团队认为某次修改对服务 A 无影响,所以没告诉服务 A 的开发团队,而实际上是有影响的;服务 B 的开发团队认为某次修改对服务 A 有影响,而服务 A 的开发团队认为无影响;服务 B 的开发团队忘记把某次修改同步到服务 A 的开发团队。</p>
|
||
|
||
<p>所以,比较好的方式就是<strong>通过“契约”来降低服务 A 和服务 B 的依赖</strong>。具体指导原则为:</p>
|
||
|
||
<ul>
|
||
|
||
<li>根据服务 A 和服务 B 的交互生成一份“契约”,且契约内容的变化可以及时感知到,并生成模拟服务;</li>
|
||
|
||
<li>将服务之间的集成测试,变成两个测试,即真实的服务 A 和模拟服务 B 之间的测试和模拟的服务 A 和真实服务 B 之间的测试。</li>
|
||
|
||
</ul>
|
||
|
||
<p><img src="assets/Ciqc1F8rwi2AD_NcAABULdvxmSY140.png" alt="Drawing 6.png" /></p>
|
||
|
||
<p>契约测试示意图</p>
|
||
|
||
<p>理解了契约测试产生的背景,我们来讲解下微服务架构下契约测试的具体含义。</p>
|
||
|
||
<h3>契约测试介绍</h3>
|
||
|
||
<p>在微服务架构下,契约(Contract)是指服务的消费者(Consumer)与服务的提供者(Provider)之间交互协作的约定。契约主要包括两部分。</p>
|
||
|
||
<ul>
|
||
|
||
<li><strong>请求(Request)</strong>:指消费者发出的请求,通常包括请求头(Header)、请求内容(URI、Path、HTTP Verb)、请求参数及取值类型和范围等。</li>
|
||
|
||
<li><strong>响应(Response)</strong>:指提供者返回的响应。可能包括响应的状态码(Status Word)、响应体的内容(XML/JSON) 或者错误的信息描述等。</li>
|
||
|
||
</ul>
|
||
|
||
<p>契约测试(Contract Test)是将契约作为中间标准,对消费者与提供者间的协作进行的验证。根据测试对象的不同,又分为两种类型:<strong>消费者驱动 和 提供者驱动。最常用的是消费者驱动的契约测试</strong>(Consumer-Driven Contract Test,简称 CDC)。核心思想是从消费者业务实现的角度出发,由消费者端定义需要的数据格式以及交互细节,生成一份契约文件。然后生产者根据契约文件来实现自己的逻辑,并在持续集成环境中持续验证该实现结果是否正确。</p>
|
||
|
||
<p>为什么要进行<strong>消费者驱动</strong>的契约测试呢?在微服务架构下,提供者和消费者往往是一对多的关系。比如,服务提供者提供了一个 API,该服务会被多个不同的消费者所调用,当提供者想要修改该 API 时,就需要知道该 API 当前正在被多少消费者所调用,具体是怎样调用的。否则,当提供者针对该 API 进行逻辑或字段的修改(新增、删除、更新)时,都有可能导致消费者无法正常使用。而消费者驱动的契约测试相当于把不同消费者对该 API 的需求暴露出来,形成契约文件和验证点,提供者完成功能修改后对修改结果进行验证,以保障符合消费者的预期。</p>
|
||
|
||
<p>工欲善其事,必先利其器。要想做某类测试,一个好的测试框架是必不可少的。在契约测试领域也有不少测试框架,其中两个比较成熟的企业级测试框架:</p>
|
||
|
||
<ul>
|
||
|
||
<li>Spring Cloud Contract,它是 Spring 应用程序的消费者契约测试框架;</li>
|
||
|
||
<li>Pact 系列框架,它是支持多种语言的框架。</li>
|
||
|
||
</ul>
|
||
|
||
<p>因为 Pact 的多语言特性,它也是实际工作过程中使用最频繁的框架。为了加深对契约测试的理解,我们来看一个基于 Pact 框架的契约测试的实例。</p>
|
||
|
||
<h3>契约测试实例</h3>
|
||
|
||
<h4>契约内容</h4>
|
||
|
||
<p>如下所示,服务提供者为 userservice,消费者为 ui,契约内容包含了 POST 请求 /user-service/users,传参为对象 user, 并返回 201 和创建用户的 id。</p>
|
||
|
||
<pre><code>{
|
||
|
||
|
||
|
||
"consumer": {
|
||
|
||
|
||
|
||
"name": "ui"
|
||
|
||
|
||
|
||
},
|
||
|
||
|
||
|
||
"provider": {
|
||
|
||
|
||
|
||
"name": "userservice"
|
||
|
||
|
||
|
||
},
|
||
|
||
|
||
|
||
"interactions": [
|
||
|
||
|
||
|
||
{
|
||
|
||
|
||
|
||
"description": "a request to POST a person",
|
||
|
||
|
||
|
||
"providerState": "provider accepts a new person",
|
||
|
||
|
||
|
||
"request": {
|
||
|
||
|
||
|
||
"method": "POST",
|
||
|
||
|
||
|
||
"path": "/user-service/users",
|
||
|
||
|
||
|
||
"headers": {
|
||
|
||
|
||
|
||
"Content-Type": "application/json"
|
||
|
||
|
||
|
||
},
|
||
|
||
|
||
|
||
"body": {
|
||
|
||
|
||
|
||
"firstName": "Arthur",
|
||
|
||
|
||
|
||
"lastName": "Dent"
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
},
|
||
|
||
|
||
|
||
"response": {
|
||
|
||
|
||
|
||
"status": 201,
|
||
|
||
|
||
|
||
"headers": {
|
||
|
||
|
||
|
||
"Content-Type": "application/json"
|
||
|
||
|
||
|
||
},
|
||
|
||
|
||
|
||
"body": {
|
||
|
||
|
||
|
||
"id": 42
|
||
|
||
|
||
|
||
},
|
||
|
||
|
||
|
||
"matchingRules": {
|
||
|
||
|
||
|
||
"$.body": {
|
||
|
||
|
||
|
||
"match": "type"
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
],
|
||
|
||
|
||
|
||
"metadata": {
|
||
|
||
|
||
|
||
"pactSpecification": {
|
||
|
||
|
||
|
||
"version": "2.0.0"
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<h4>Spring Controller</h4>
|
||
|
||
<p>创建 Spring Controller,并遵循上述的契约;</p>
|
||
|
||
<pre><code>@RestController
|
||
|
||
|
||
|
||
public class UserController {
|
||
|
||
|
||
|
||
private UserRepository userRepository;
|
||
|
||
|
||
|
||
@Autowired
|
||
|
||
|
||
|
||
public UserController(UserRepository userRepository) {
|
||
|
||
|
||
|
||
this.userRepository = userRepository;
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
@PostMapping(path = "/user-service/users")
|
||
|
||
|
||
|
||
public ResponseEntity<IdObject> createUser(@RequestBody @Valid User user) {
|
||
|
||
|
||
|
||
User savedUser = this.userRepository.save(user);
|
||
|
||
|
||
|
||
return ResponseEntity
|
||
|
||
|
||
|
||
.status(201)
|
||
|
||
|
||
|
||
.body(new IdObject(savedUser.getId()));
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<h4>服务提供者测试</h4>
|
||
|
||
<p>为了快速发现问题,最好在每次构建时都进行契约测试,可以使用 Junit 来管理测试。</p>
|
||
|
||
<p>要创建 Junit 测试,需要添加依赖到工程中:</p>
|
||
|
||
<pre><code>dependencies {
|
||
|
||
|
||
|
||
testCompile("au.com.dius:pact-jvm-provider-junit5_2.12:3.5.20")
|
||
|
||
|
||
|
||
// Spring Boot dependencies omitted
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>创建服务提供者测试 UserControllerProviderTest,并运行:</p>
|
||
|
||
<pre><code>@ExtendWith(SpringExtension.class)
|
||
|
||
|
||
|
||
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
|
||
|
||
|
||
|
||
properties = "server.port=8080")
|
||
|
||
|
||
|
||
@Provider("userservice")
|
||
|
||
|
||
|
||
@PactFolder("../pact-angular/pacts")
|
||
|
||
|
||
|
||
public class UserControllerProviderTest {
|
||
|
||
|
||
|
||
@MockBean
|
||
|
||
|
||
|
||
private UserRepository userRepository;
|
||
|
||
|
||
|
||
@BeforeEach
|
||
|
||
|
||
|
||
void setupTestTarget(PactVerificationContext context) {
|
||
|
||
|
||
|
||
context.setTarget(new HttpTestTarget("localhost", 8080, "/"));
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
@TestTemplate
|
||
|
||
|
||
|
||
@ExtendWith(PactVerificationInvocationContextProvider.class)
|
||
|
||
|
||
|
||
void pactVerificationTestTemplate(PactVerificationContext context) {
|
||
|
||
|
||
|
||
context.verifyInteraction();
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
@State({"provider accepts a new person"})
|
||
|
||
|
||
|
||
public void toCreatePersonState() {
|
||
|
||
|
||
|
||
User user = new User();
|
||
|
||
|
||
|
||
user.setId(42L);
|
||
|
||
|
||
|
||
user.setFirstName("Arthur");
|
||
|
||
|
||
|
||
user.setLastName("Dent");
|
||
|
||
|
||
|
||
when(userRepository.findById(eq(42L))).thenReturn(Optional.of(user));
|
||
|
||
|
||
|
||
when(userRepository.save(any(User.class))).thenReturn(user);
|
||
|
||
|
||
|
||
}
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<p>测试结果如下所示:</p>
|
||
|
||
<pre><code>Verifying a pact between ui and userservice
|
||
|
||
|
||
|
||
Given provider accepts a new person
|
||
|
||
|
||
|
||
a request to POST a person
|
||
|
||
|
||
|
||
returns a response which
|
||
|
||
|
||
|
||
has status code 201 (OK)
|
||
|
||
|
||
|
||
includes headers
|
||
|
||
|
||
|
||
"Content-Type" with value "application/json" (OK)
|
||
|
||
|
||
|
||
has a matching body (OK)
|
||
|
||
</code></pre>
|
||
|
||
<p>也可以将契约文件上传到 PactBroker 中,这样后续测试时可以直接从 PactBroker 中加载契约文件:</p>
|
||
|
||
<pre><code>@PactBroker(host = "host", port = "80", protocol = "https",
|
||
|
||
|
||
|
||
authentication = @PactBrokerAuth(username = "username", password = "password"))
|
||
|
||
|
||
|
||
public class UserControllerProviderTest {
|
||
|
||
|
||
|
||
...
|
||
|
||
|
||
|
||
}
|
||
|
||
</code></pre>
|
||
|
||
<h3>总结</h3>
|
||
|
||
<p>本节课我首先讲解了契约的定义,通俗地讲,它是双方或多方共识的一种约定,需要协同方共同遵守。而在微服务架构下,<strong>契约(Contract)是指服务的消费者(Consumer)与服务的提供者(Provider)之间交互协作的约定,主要包括请求和响应两部分。</strong></p>
|
||
|
||
<p>紧接着讲解了微服务架构下跨服务测试的痛点和难点,因而引入了契约测试的概念,它的指导思想是<strong>通过“契约”来降低服务和服务之间的依赖</strong>,即,将契约作为中间标准,对消费者与提供者间的协作进行的验证。根据测试对象的不同,契约测试分为两种,但最常用的契约测试类型是<strong>消费者驱动的契约测试</strong>(Consumer-Driven Contract Test,简称 CDC)。核心思想是<strong>从消费者业务实现的角度出发</strong>,由消费者端定义需要的数据格式以及交互细节,生成一份契约文件。然后提供者根据契约文件来实现自己的逻辑,并在持续集成环境中持续验证该实现结果是否正确。契约测试框架也有多种,但最常见的框架有 Spring Cloud Contract 和 Pact,其中 Pact 框架更为流行。</p>
|
||
|
||
<p>最后给出了基于 Pact 框架的契约测试实例的大体步骤,并在文稿下方给出了示例代码地址,感兴趣的同学可以自行学习。</p>
|
||
|
||
<p>你所负责的项目或服务,是否进行过契约测试呢?如果有,是哪种类型的契约测试,具体的进展是怎样的?欢迎在留言区评论。同时欢迎你能把这篇文章分享给你的同学、朋友和同事,大家一起交流。</p>
|
||
|
||
<blockquote>
|
||
|
||
<p>相关链接
|
||
|
||
https://www.martinfowler.com/articles/microservice-testing/
|
||
|
||
https://reflectoring.io/7-reasons-for-consumer-driven-contracts/
|
||
|
||
契约测试框架
|
||
|
||
https://docs.pact.io/
|
||
|
||
https://spring.io/projects/spring-cloud-contract
|
||
|
||
https://www.infoq.com/news/2019/02/contract-testing-microservices/
|
||
|
||
实例
|
||
|
||
https://github.com/thombergs/code-examples/tree/master/pact/pact-spring-provider
|
||
|
||
https://reflectoring.io/consumer-driven-contract-provider-pact-spring/</p>
|
||
|
||
</blockquote>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
<div>
|
||
|
||
<div style="float: left">
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/06 组件测试:如何保证单服务的质量?.md">上一页</a>
|
||
|
||
</div>
|
||
|
||
<div style="float: right">
|
||
|
||
<a href="/专栏/微服务质量保障 20 讲-完/08 端到端测试:站在用户视角验证整个系统.md">下一页</a>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
</div>
|
||
|
||
|
||
|
||
<a class="off-canvas-overlay" onclick="hide_canvas()"></a>
|
||
|
||
</div>
|
||
|
||
<script defer src="https://static.cloudflareinsights.com/beacon.min.js/v652eace1692a40cfa3763df669d7439c1639079717194" integrity="sha512-Gi7xpJR8tSkrpF7aordPZQlW2DLtzUlZcumS8dMQjwDHEnw9I7ZLyiOj/6tZStRBGtGgN6ceN6cMH8z7etPGlw==" data-cf-beacon='{"rayId":"709978e31a3d3cfa","version":"2021.12.0","r":1,"token":"1f5d475227ce4f0089a7cff1ab17c0f5","si":100}' crossorigin="anonymous"></script>
|
||
|
||
</body>
|
||
|
||
<!-- Global site tag (gtag.js) - Google Analytics -->
|
||
|
||
<script async src="https://www.googletagmanager.com/gtag/js?id=G-NPSEEVD756"></script>
|
||
|
||
<script>
|
||
|
||
window.dataLayer = window.dataLayer || [];
|
||
|
||
|
||
|
||
function gtag() {
|
||
|
||
dataLayer.push(arguments);
|
||
|
||
}
|
||
|
||
|
||
|
||
gtag('js', new Date());
|
||
|
||
gtag('config', 'G-NPSEEVD756');
|
||
|
||
var path = window.location.pathname
|
||
|
||
var cookie = getCookie("lastPath");
|
||
|
||
console.log(path)
|
||
|
||
if (path.replace("/", "") === "") {
|
||
|
||
if (cookie.replace("/", "") !== "") {
|
||
|
||
console.log(cookie)
|
||
|
||
document.getElementById("tip").innerHTML = "<a href='" + cookie + "'>跳转到上次进度</a>"
|
||
|
||
}
|
||
|
||
} else {
|
||
|
||
setCookie("lastPath", path)
|
||
|
||
}
|
||
|
||
|
||
|
||
function setCookie(cname, cvalue) {
|
||
|
||
var d = new Date();
|
||
|
||
d.setTime(d.getTime() + (180 * 24 * 60 * 60 * 1000));
|
||
|
||
var expires = "expires=" + d.toGMTString();
|
||
|
||
document.cookie = cname + "=" + cvalue + "; " + expires + ";path = /";
|
||
|
||
}
|
||
|
||
|
||
|
||
function getCookie(cname) {
|
||
|
||
var name = cname + "=";
|
||
|
||
var ca = document.cookie.split(';');
|
||
|
||
for (var i = 0; i < ca.length; i++) {
|
||
|
||
var c = ca[i].trim();
|
||
|
||
if (c.indexOf(name) === 0) return c.substring(name.length, c.length);
|
||
|
||
}
|
||
|
||
return "";
|
||
|
||
}
|
||
|
||
|
||
|
||
</script>
|
||
|
||
|
||
|
||
</html>
|
||
|