契约测试的背景
随着敏捷流程和DevOps的盛行,大项目的发版上线流程变得越来越笨重,在这种要求快速发布快速迭代的项目里,微服务的优势凸显无疑。 一个大项目按照功能或者分类等某一类共性拆成多个子组件,每个组件独立维护、测试、发布,敏捷流程容易推动,开发、测试、产品的工作也相对轻松。原本可能一个月只能发2个版本的产品,在微服务架构下可能发N个版本。
微服务架构下的困境
微服务架构固然有自己天然的优势,不过微服务架构特点,也给开发测试带来了不小的挑战: - 第一,维护成本的提升。原来3个大的系统,现在变成了30个功能代码组件,每个组件需要专人维护,需要提供稳定测试环境,关联查找和分析是一件麻烦事; - 第二,系统复杂性的提升。接上,彼此之间的调用关系指数级上升,原先一个接口的调用可能只会涉及到2个系统,在微服务的架构下,会调用10+的组件,复杂性较高。基于此,想要维护一套完整的、不受干扰的测试环境很难实现
传统接口测试的局限性
大部分场景下面,我们通常依靠接口测试来测试系统级的接口功能正确性,但在微服务背景下,接口测试有较大的局限性。
一,需要一套完整的测试环境;
二,接口测试用例的冗余度指数级增加,原本大模块设计出来的用例,在微服务下面需要针对每个组件都得单独设计测试用例,设计、维护、调用成本都会变得很高;
三,测试用例设计要求高,理论上用例需要覆盖到存在调用关系的所有逻辑分支,这需要你对组件调用关系有很清晰的了解。
契约测试
为了解决传统接口测试在微服务结构下的局限性,引入契约测试是很有必要的。
契约测试,Consumer Driven Contracts testing,消费者驱动测试。
在契约测试的理念下,只有两个对象:生产者和消费者,也就是调用者和被调用者,即细化到最小的一个系统组件。契约测试最精髓的思想是测试左移,测试只需要提供组件级的测试用例即可,执行全部交给开发和CI即可。 契约测试可靠吗?是可靠地,前提是大家在契约测试上达成共识,即无论微服务下面有多少组件,只要每个组件完成各自的契约测试,我们就有理由相信,组件集成之后的服务的质量是可靠的。
实例说明
我们用实例更好的理解一下契约测试1、有一个获取用户信息的接口 /user,参数name,调用地址:http://0.0.0.0:5000/user?name=zhoujielun,返回对应的个人信息普通的接口测试,从客户端出发,通过设计不同的参数组合尽可能的覆盖更多的逻辑分支,比如name=zhoujielun,name=123等等,为了说明契约测试,我们尽量简单说明使用unittest单测框架来做接口测试,我们关注接口的返回代码如 user_unittest.py所示
def test_lizeyang(self):
"""name=lizeyang的测试用例"""
expect = { "age": 21, "home": "china"}
res = requests.get("http://127.0.0.1:5000/user?name=lizeyang").json()
self.assertEqual(res, expect)
def test_zhoujielun(self):
"""name=zhoujielun的测试用例"""
expect = { "age": 21, "home": "america"}
res = requests.get("http://127.0.0.1:5000/user?name=zhoujielun").json()
self.assertEqual(res, expect)
self.assertEqual(res, expect)
运行python user_unittest.py,结果如下:
======================================================================
FAIL: test_lizeyang (__main__.UserTesting)
name=lizeyang的测试用例
----------------------------------------------------------------------
Traceback (most recent call last):
File "user_unittest.py", line 19, in test_lizeyang
self.assertEqual(res, expect)
AssertionError: {u'errmsg': u'user not exist.'} != {'home': 'china', 'age': 21}
- {u'errmsg': u'user not exist.'}
+ {'age': 21, 'home': 'china'}
----------------------------------------------------------------------
Ran 2 tests in 0.015s
FAILED (failures=1)
可以看到,name=lizeyang的测试用例是失败的,预期结果和实际调用结果不相符
上面是一个简单的接口测试用例,对于普通服务的接口测试来说,这种测试方法是可行的,没有问题但是在微服务架构下,会遇到一个问题:某服务X改了接口返回的数据或者结构,比如原先的返回code字段变更成了status或者某个home字段的值,按照上面的测试方法,我们独立对X测试的时候,优先考虑的是X服务的可用性和正确性,很难去实际度量X的上游调用方他的代码在实际调用X接口的时候是否正常,服务他的预期,基于此,微服务的一个组件,他的上游组件数量是N多。
生产者按照自己的意愿去生产接口,导致这些接口无法满足消费者的需求,从而引发各种开发、测试问题,显然不是我们想要的结果。契约测试的出现就是为了解决这种需求与实际不对等的问题,本质上其实是一个基于mock服务的接口测试
通俗的契约测试核心思想:对于任意两个存在调用关系的服务组件A和B,A需要调用B的接口api_01,从契约的角度出发,我们需要从A这个消费者的角度制造一个契约(或者叫合同吧),合同里面约定了接口、接口入参以及在入参的前提接口应该给出什么样的返回。然后B组件的接口api_01在开发的时候需要按照契约的要求去开发,开发完成之后由B端人员发起契约测试或者CI集成测试
契约测试 = 测试左移 + mock + 接口测试
#!/usr/bin/python
# -*- encoding:utf-8 -*-
import atexit
import requests
import unittest
from pact import EachLike, SomethingLike, Term
from pact.consumer import Consumer
from pact.provider import Provider
# 定义一个pact,消费者是ModuleA,生产者是ModuleB,契约文件存放在pacts文件夹下
pact = Consumer('ModuleA').has_pact_with(Provider('ModuleB'), pact_dir='./pacts’)
# 启动服务
pact.start_service()
atexit.register(pact.stop_service)
class UserTesting(unittest.TestCase):
def runTest(self):
self.test_lizeyang()
def test_lizeyang(self):
# 预期结果
expected = {"age": 222, "home": "china"}
# 契约的实际内容
(pact
.given('test lizeyang this user.')
.upon_receiving('a request for the user `lizeyang`')
.with_request('get', '/user', query={"name": "lizeyang"})
.will_respond_with(200, body=expected))
# 调用pact自带的mock服务,注册接口
with pact:
res = requests.get("http://localhost:1234/user?name=lizeyang").json()
self.assertEqual(res, expected)
if __name__ == "__main__":
ut = UserTesting()
ut.test_lizeyang()
运行python user_pact_test.py之后,
INFO WEBrick 1.3.1
INFO ruby 2.2.2 (2015-04-13) [x86_64-darwin13]
INFO WEBrick::HTTPServer#start: pid=63533 port=1234
{u'home': u'china', u'age': 222}
INFO going to shutdown ...
INFO WEBrick::HTTPServer#start done.
这时,在contracts/pacts文件夹下会自动生产一个契约文件modulea-moduleb.json,内容如下。
{
"consumer": {
"name": "ModuleA"
},
"provider": {
"name": "ModuleB"
},
"interactions": [
{
"description": "a request for the user `lizeyang`”, # 描述"providerState": "test lizeyang this user.”,
"request": {
"method": "get",
"path": "/user",
"query": "name=lizeyang"
},
"response": {
"status": 200,
"headers": {
},
"body": {
"home": "china",
"age": 222
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "2.0.0"
}
}
}
这个契约文件定义了调用生产者ModuleB时候的测试用例和返回报文的格式 B端拿到这份契约之后,需要按照个契约的内容完成接口开发以及自测 自测方法:
pact-verifier --provider-base-url=http://127.0.0.1:5000 --pact-url=./contracts/pacts/test_sender-service_001.json
可以看到自测接口与实际的返回是不一致的,开发需要按照契约修改自己的代码来通过契约测试即可。 至此,这个Python的契约测试实例就完成了,当然了契约测试的内容不仅仅只有这样,还包括契约测试的数据文件和版本管理,这个后面有机会再单独讲讲吧。