最近,公司正在推动整个质量体系的改革工作,其中一项改革议题就是新增契约测试这一专项测试内容。
为了搞懂契约测试的来龙去脉进而可以用相应的工具将契约测试落地,笔者很是花了一番工夫。因而想动笔将这一探索的结果记录下来,分享给更多的小伙伴们。
提起契约测试,就不得不说近两三年大行其道的微服务。“微服务”是一种架构概念,他的目的是通过将功能分解到各个离散的服务中以实现对解决方案的解耦,从而降低系统的耦合性,并提供更加灵活的服务支持。
什么?听不懂.....好吧,来张图直观一点:
图1 单体式开发
图2 基于微服务架构的开发
图1是传统的WEB开发模式--单体式开发,它把所有的功能都打包在一个WAR包里,然后部署在一个诸如TOMCAT之类的容器内,它基本上没有任何外部依赖,便于集中式管理。
但是它也有自己的缺点:
比如开发效率低,因为各个模块之间可能相互等待;
比如维护难,因为所有功能代码都耦合在一起,不方便BUG的定位和代码调试;
又比如稳定性不好,一个微小的BUG都可能牵一发而动全身。
图2就说基于微服务架构的开发模式,它把一个复杂的大型系统拆分成一个个独立的服务,每一个微服务看成是用一组API提供业务功能的组件,每个微服务由一个团队来开发维护,各个微服务之间使用HTTP API进行通讯,服务可以采用不同的编程语言与数据库。
这样的架构不仅可以有效提高团队的整体工作效率,还可以通过这种松耦合的关系(不同服务的代码分离开来),方便BUG的定位和代码的调试,提高系统的复用性和稳定性。
接下来的一个问题就是:为什么采用微服务就需要契约测试呢?
事实就是:当把一个复杂系统拆解成一个个微服务后,会出现服务之间的相互调用关系变得错综复杂。比如同一个微服务A同时被微服务B、微服务C、微服务D、微服务E......所调用。
当作为服务提供者的微服务A发生改变时,怎么保证这种更改对其它所有使用者造成的影响都被感知到了呢?这时契约测试就派上用场了。
契约测试 ,又称之为消费者驱动的契约测试(Consumer-Driven Contracts,简称CDC),根据消费者驱动契约,我们可以将服务分为消费者端和生产者端,而消费者驱动的契约测试的核心思想在于是从消费者业务实现的角度出发,由消费者自己会定义需要的数据格式以及交互细节,并驱动生成一份契约文件。
然后生产者根据契约文件来实现自己的逻辑,并在持续集成环境中持续验证。
以消费者B为例,它制定与生产者A之间数据交互的协议、格式等方面的内容,并生成契约文件。此时A按照契约文件来提供服务就好。这样A、B都按契约行事,只关注自己的部分就可以达成整个系统对它们的要求了。
说起契约测试的特点,首先要介绍下它与单元测试、API测试、集成测试的区别。
对软件中的最小可测试单元进行检查和验证。对于单元测试中单元的含义,一般来说,要根据实际情况去判定其具体含义,如C语言中单元指一个函数,Java里单元指一个类,图形化的软件中可以指一个窗口或一个菜单等。
总的来说,单元就是人为规定的最小的被测功能模块。
对软件中的业务接口进行测试,着重关注接口的请求格式、调用参数、接口返回的内容、接口的性能、接口的安全性等待。比如:测试接口的功能是否按接口文档中定义的那样完全实现了,对异常请求有没有合适的处理方式;接口最大并发数是否满足业务需求;接口的是否有安全漏洞等等。
为了测试服务之间连接调用的正确性,为了验证服务提供者的功能是不是真正能够满足消费者的需求。它其实体现了测试前移的思想,把本来要通过集成测试才能验证的工作化作单元测试和接口测试,用更轻量的方式快速进行验证。
站在用户的角度来验证整个系统功能的正确性,它测试的对象是端到端的业务流程,并融入用户故事和真实数据,很显然它的业务价值是最高的。
总之,契约测试体现的是一种测试前移的思想,它测试的对象是服务之间链路的正确性,是对传统单元测试、API测试、集成测试的有力补充。
图 3
单元测试、契约测试、端到端测试的区别与联系
可以使得生产端和消费端之间测试解耦,不再需要等到系统联调才能发现问题。尤其在某些大型互联网项目中,联调可能会涉及不同国家和地区的多个团队进行,相当耗时耗力,如果能通过契约测试提前发现问题,可以减少很多不必要的沟通成本。
完全由消费者驱动的方式,消费者需要什么数据,服务端就给什么样的数据,数据契约也是由消费者来定的。
这说明契约测试更加注重业务需求和消费端体验,以业务为中心促进质量的全面提升。
测试前移,越早地发现问题,保证后续测试的完整性。在软件测试行业有这么一条公认规则:BUG发现得越早,其修复的代价越低。
契约测试让测试前移,就降低了BUG的修复成本,保证后续测试的完整性。
通过契约测试,团队能以一种离线的方式(不需要消费者、提供者同时在线),通过契约作为中间的标准,验证提供者提供的内容是否满足消费者的期望。
Pact是一个开源框架,最早是由澳洲最大的房地产信息提供商REA Group的开发者及咨询师们共同创造。
Pact工具于2013年开始开源,发展到今天已然形成了一个小的生态圈,包括各种语言(Ruby/Java/.NET/JavaScript/Go/Scala/Groovy...)下的Pact实现,契约文件共享工具Pact Broker等。
Pact的用户已经遍及包括RedHat、IBM、Accenture等在内的若干知名公司,Pact已经是事实上的契约测试方面的业界标准。
Step1. 编写测试用例,生成契约文件。
在消费者端Consumer写一个对接口发送请求的单元测试,在运行这个单元测试的时候,Pact会将服务提供者自动用一个MockService代替,并自动生成契约文件,这个契约文件是Json形式的。
Step2. 利用pact-verifier命令和契约文件,验证接口提供者是否正确。
在服务提供端Provider做契约验证测试,将Provider服务启动起来以后,通过Pact插件可以运行一个命令,比如你是用maven,就是mvn pact:verify,它会自动按照契约生成接口请求并验证接口响应是否满足契约中的预期。
因此,我们可以看到在这个过程中,在消费者端不用启动Provider,在服务提供端不用启动Consumer,却完成了与集成测试类似的验证。
下面给一个DEMO方便大家理解这一过程。
1.编写服务提供者A的代码:
import json
from flask import Flask
test_app = Flask(__name__)
def get_information():
information = {
"size": "M",
"color": "red"
}
return json.dumps(information)
if __name__ == '__main__':
test_app.run(port=8088)
(左右滑动查看完整代码)
2.编写服务消费者B的代码:
import json
import requests
from flask import Flask
test_app = Flask(__name__)
def show_information():
res = requests.get("http://localhost:8088/").json()
result = {
"code":0,
"msg":"ok",
"data": res
}
return json.dumps(result)
if __name__ == '__main__':
test_app.run(port=8089)
(左右滑动查看完整代码)
3.编写测试用例的代码:
import atexit
import requests
import unittest
from pact.consumer import Consumer
from pact.provider import Provider
# 定义一个pact,消费者是B,生产者是A,契约文件存放在pacts文件夹下
pact = Consumer('B').has_pact_with(Provider('A'), pact_dir='./pacts')
# 启动pact服务
pact.start_service()
atexit.register(pact.stop_service)
# 测试用例
class UserTesting(unittest.TestCase):
def test_service(self):
# 消费者定义的期望结果
expected = {"size": "M", "color": "red"}
# 消费者定义的契约的实际内容。包括请求参数、请求方法、请求头、响应值等
(pact
.given('test service.')
.upon_receiving('a request for B')
.with_request('get', '/')
.will_respond_with(200, body=expected))
# pact自带一个mock服务,端口 1234
# 用requests向mock接口发送请求,验证mock的结果是否正确
with pact:
res = requests.get("http://localhost:1234").json()
self.assertEqual(res, expected)
if __name__ == "__main__":
Utest = UserTesting()
Utest.test_service()
(左右滑动查看完整代码)
4.生成实际的契约文件b-a.json:
{
"consumer": {
"name": "B"
},
"provider": {
"name": "A"
},
"interactions": [
{
"description": "a request for B",
"providerState": "test service.",
"request": {
"method": "get",
"path": "/"
},
"response": {
"status": 200,
"headers": {
},
"body": {
"size": "M",
"color": "red"
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "1.0"
}
}}
(左右滑动查看完整代码)
5.根据契约文件,验证服务A是否正确。
a. 启动服务A(提供者);
b. 执行pact-verifier --provider-base-url=http://127.0.0.1:8088 --pact-url=./pacts/b-a.json,即可验证接口是否符合契约。
本文从契约测试的由来、契约测试的定义、契约测试的特点、契约测试的作用和契约测试的实战工具Pact等五个方面全方位多维度地阐述了契约测试,既有理论原理的描述,也有具体的代码实现,希望能给大家带来一点启发!