learn.lianglianglee.com/专栏/微服务质量保障 20 讲-完/07 契约测试:如何进行消费者驱动的契约测试?.md.html
2022-05-11 18:52:13 +08:00

1125 lines
25 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!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>{
&quot;consumer&quot;: {
&quot;name&quot;: &quot;ui&quot;
},
&quot;provider&quot;: {
&quot;name&quot;: &quot;userservice&quot;
},
&quot;interactions&quot;: [
{
&quot;description&quot;: &quot;a request to POST a person&quot;,
&quot;providerState&quot;: &quot;provider accepts a new person&quot;,
&quot;request&quot;: {
&quot;method&quot;: &quot;POST&quot;,
&quot;path&quot;: &quot;/user-service/users&quot;,
&quot;headers&quot;: {
&quot;Content-Type&quot;: &quot;application/json&quot;
},
&quot;body&quot;: {
&quot;firstName&quot;: &quot;Arthur&quot;,
&quot;lastName&quot;: &quot;Dent&quot;
}
},
&quot;response&quot;: {
&quot;status&quot;: 201,
&quot;headers&quot;: {
&quot;Content-Type&quot;: &quot;application/json&quot;
},
&quot;body&quot;: {
&quot;id&quot;: 42
},
&quot;matchingRules&quot;: {
&quot;$.body&quot;: {
&quot;match&quot;: &quot;type&quot;
}
}
}
}
],
&quot;metadata&quot;: {
&quot;pactSpecification&quot;: {
&quot;version&quot;: &quot;2.0.0&quot;
}
}
}
</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 = &quot;/user-service/users&quot;)
public ResponseEntity&lt;IdObject&gt; 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(&quot;au.com.dius:pact-jvm-provider-junit5_2.12:3.5.20&quot;)
// Spring Boot dependencies omitted
}
</code></pre>
<p>创建服务提供者测试 UserControllerProviderTest并运行</p>
<pre><code>@ExtendWith(SpringExtension.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
properties = &quot;server.port=8080&quot;)
@Provider(&quot;userservice&quot;)
@PactFolder(&quot;../pact-angular/pacts&quot;)
public class UserControllerProviderTest {
@MockBean
private UserRepository userRepository;
@BeforeEach
void setupTestTarget(PactVerificationContext context) {
context.setTarget(new HttpTestTarget(&quot;localhost&quot;, 8080, &quot;/&quot;));
}
@TestTemplate
@ExtendWith(PactVerificationInvocationContextProvider.class)
void pactVerificationTestTemplate(PactVerificationContext context) {
context.verifyInteraction();
}
@State({&quot;provider accepts a new person&quot;})
public void toCreatePersonState() {
User user = new User();
user.setId(42L);
user.setFirstName(&quot;Arthur&quot;);
user.setLastName(&quot;Dent&quot;);
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
&quot;Content-Type&quot; with value &quot;application/json&quot; (OK)
has a matching body (OK)
</code></pre>
<p>也可以将契约文件上传到 PactBroker 中,这样后续测试时可以直接从 PactBroker 中加载契约文件:</p>
<pre><code>@PactBroker(host = &quot;host&quot;, port = &quot;80&quot;, protocol = &quot;https&quot;,
authentication = @PactBrokerAuth(username = &quot;username&quot;, password = &quot;password&quot;))
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>