原文:微服务下的契约测试—Python版 - 知乎 (zhihu.com)
契约测试--pact框架使用 - 简书 (jianshu.com)
我们用实例更好的理解一下契约测试
- 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

可以看到自测接口与实际的返回是不一致的,开发需要按照契约修改自己的代码来通过契约测试即可。