利用PACT做契约测试

474 阅读7分钟

我正在参加「掘金·启航计划」

针对契约测试市面上提供了很多工具,比较出名的就是PACT,它是一个开源的工具,支持多语言使用。本文就以Python-Pact为例,给大家介绍一下如何使用PACT进行契约测试

PACT是什么?

Pact框架家族提供对消费者驱动的契约测试的支持。

消费者驱动的契约

契约是在客户端(消费者)与API端(生产者)之间的一组约定,描述了两者之间所发生的交互。

消费者驱动的契约是一种从消费者视角来驱动生产者开发的模式。

Pact是一种可用于测试契约符合预期的测试工具。

该工具支持多语言

  • Ruby Pact

  • JVM Pact 和 Scala-Pact

  • .NET Pact

  • JS Pact

  • Go Pact

  • Swift / Objective-C Pact

  • Python

Pact是怎样工作的?

  1. 在生产者所面向的消费者项目代码中编写测试,期望响应设置于模拟的服务生产者上。

  2. 在测试运行时,模拟的服务将返回所期望的响应。请求和所期望的响应将会被写入到一个"pact"文件中。

  3. pact文件中的请求随后在生产者上进行重放,并检查实际响应以确保其与所期望响应相匹配。

编辑

编辑

术语表

1.服务消费者

服务消费者是指向另一组件(服务生产者)发起HTTP请求的组件。注意这并不依赖于数据的发送方式——无论是GET还是PUT / POST / PATCH,消费者都是HTTP请求的发起者。

2.服务生产者

服务生产者是指向另一组件(服务消费者)的HTTP请求提供响应的服务器。

3.模拟服务生产者

模拟服务生产者用于在消费者项目中的单元测试里模拟真实的服务生产者,意味着不必需要真实的服务生产者就绪,就可以将类集成测试运行起来。

4.Pact文件

Pact文件是指一个含有消费者测试中所定义的请求和响应被序列化后的JSON的文件。即契约。

5.契约验证

要对一个Pact进行验证,就要对Pact文件中所包含的请求基于生产者代码进行重放,然后检查返回的响应,确保其与Pact文件中所期望响应相匹配。

6.生产者状态

在对生产者重放某个给定的请求时,一个用于描述此时生产者应具有的“状态”(类似于夹具)的名字——比如“when user John Doe exists”或“when user John Doe has a bank account”。

生产者状态的名字是在写消费者测试时被指定的,之后当运行生产者的pact验证时,这个名字将被用于唯一标识在请求执行前应运行的代码块。

7.Pact规范

Pact规范是一份用于控制实际生成的Pact文件结构的文档,允许不同语言之间的互操作性(例如,设想一个JavaScript实现的消费者连接到基于Scala JVM的生产者),并使用语义版本控制来指示具有破坏性的变更。

Pact每种语言的实现都要实现规范中的规则,并且明确说明支持哪个或哪些版本,主要对应于哪些特性是可用的。

契约测试实践

编辑

1.开发生产者/消费者服务;生产者服务,利用flask实现两个接口

代码

# -*- coding: utf-8 -*-
# @Author : rain
# @Email : qualityassurance21@163.com
# @File : provider.py
# @Time : 2022/8/14 10:54

'''
    生产者端,用于生成数据内容
'''
import json

from flask import Flask, jsonify, request

app = Flask(__name__)


# 生产者接口,用于生产数据内容,提供给消费者进行数据消费
@app.route('/provider01', methods=['POST'])
def provider01():
    response = {
        'name': 'QualityAssurance',
        'description': 'writing is thinking',
        'isSuccess': True
    }
    return jsonify(response)


# 生产者接口,用于生产数据内容,提供给消费者进行数据消费
@app.route('/provider02', methods=['GET'])
def provider02():
    data = {
        'name': 'QualityAssurance',
        'description': 'writing is thinking'
    }
    return jsonify(data)


if __name__ == '__main__':
    app.run(host='127.0.0.1', port=8000, debug=True)

2.分别开发两个消费者

代码

# -*- coding: utf-8 -*-
# @Author : rain
# @Email : qualityassurance21@163.com
# @File : consumer001.py
# @Time : 2022/8/14 10:57

'''
    消费者1
'''
import json

import requests
from flask import Flask, jsonify, request

app = Flask(__name__)


# 消费者1,POST请求,用于消费生产者的数据,进行业务处理
@app.route('/consumer01', methods=['POST'])
def consumer_first():
    data = json.loads(request.data)  # 将json字符串转为dict
    name = data['name']
    body = {'name': name}
    # 获取生产者的数据内容
    resp = requests.post('http://127.0.0.1:8000/provider01', json=body).json()
    # 处理数据内容并返回
    result = {
        'msg': 'OK',
        'name': {
            'name': resp['name'],
            'description': resp['description']
        }
    }
    return jsonify(result)


if __name__ == '__main__':

app.run(host='127.0.0.1', port=5001, debug=True)

代码

# -*- coding: utf-8 -*-
# @Author : rain
# @Email : qualityassurance21@163.com
# @File : consumer001.py
# @Time : 2022/8/14 10:57

'''
    消费者2
'''
import requests
from flask import Flask, jsonify

app = Flask(__name__)


# 消费者1号,用于消费生产者的数据,进行业务处理
@app.route('/consumer02', methods=['GET'])
def consumer_second():
    # 获取生产者的数据内容
    resp = requests.get('http://127.0.0.1:8000/provider').json()
    # 处理数据内容并返回
    result = {
        'msg': 'OK',
        'name': {
            'name': resp['name'],
            'description': resp['description']
        }
    }
    return jsonify(result)

if __name__ == '__main__':
    app.run(host='127.0.0.1', port=5002, debug=True

3.利用mock的provider生成契约文件

consumer01-provider01契约文件生成代码

代码

# -*- coding: utf-8 -*-
# @Author : rain
# @Email : qualityassurance21@163.com
# @File : PactDemo01.py
# @Time : 2022/8/14 10:54

'''
    pact-python契约测试示例
'''
import atexit
import unittest

import requests
from pact import Consumer
from pact import Provider

# 定义一个契约(pact),明确消费者与生产者,明确契约文件的存放路径
pact_demo = Consumer('consumer01').has_pact_with(Provider('provider01'), pact_dir='./')
# 服务启动
pact_demo.start_service()


# 服务的注册
atexit.register(pact_demo.stop_service)
class PactDemo(unittest.TestCase):
    # 定义契约的内容
    def test_pact(self):
        # 定义预期的响应结果
        expected_res = {
            'name': 'QualityAssurance',
            'description': 'writing is thinking',
            'isSuccess': True
        }
        # 定义响应头
        headers = {
            "Content-Type": "application/json"
        }
        # 定义契约的实际内容
        (pact_demo
         .given('test service')
         .upon_receiving('for pact')
         .with_request('post', '/provider01', {'name': 'QualityAssurance'}) # 向生产者发送请求时,需要注意请求方法、路径、参数、头部信息等
         .will_respond_with(status=200, headers=headers, body=expected_res)) # 生产者在被请求之后返回的响应的结果。可以自己定义内容的

        # mock生产者,请求的实际上是pact自带的mock服务,端口默认是1234
        with pact_demo:
            data = {'name': 'QualityAssurance'}
            resp = requests.post('http://localhost:1234/provider01', json=data).json()
            print(resp)

        # 断言校验,判断预期结果是否与pact的结果相符合
        self.assertEqual(expected_res, resp)


if __name__ == '__main__':
    unittest.main(verbosity=2)

consumer02-provider02契约文件生成代码

代码

# -*- coding: utf-8 -*-
# @Author : rain
# @Email : qualityassurance21@163.com
# @File : PactDemo01.py
# @Time : 2022/8/14 10:54

'''
    pact-python契约测试示例
'''
import atexit
import unittest

import requests
from pact import Consumer
from pact import Provider

# 定义一个契约(pact),明确消费者与生产者,明确契约文件的存放路径
pact_demo = Consumer('consumer02').has_pact_with(Provider('provider02'), pact_dir='./')
# 服务启动
pact_demo.start_service()


# 服务的注册
atexit.register(pact_demo.stop_service)
class PactDemo(unittest.TestCase):
    # 定义契约的内容
    def test_pact(self):
        # 定义预期的响应结果
        expected = {
        'name': 'QualityAssurance',
        'description': 'writing is thinking'
        }
        # 定义契约的实际内容
        (pact_demo
         .given('test service')
         .upon_receiving('for pact')
         .with_request('get', '/provider02') # 向生产者发送请求时,需要注意请求方法、路径、参数、头部信息等
         .will_respond_with(status=200, body=expected)) # 生产者在被请求之后返回的响应的结果。可以自己定义内容的

        # mock生产者,请求的实际上是pact自带的mock服务,端口默认是1234
        with pact_demo:
            resp = requests.get('http://localhost:1234/provider02').json()
            print(resp)

        # 断言校验,判断预期结果是否与pact的结果相符合
        self.assertEqual(expected, resp)


if __name__ == '__main__':
    unittest.main(verbosity=2

4.执行步骤3的代码,生成契约文件

契约文件1: consumer01-provider01.json

契约文件2: consumer02-provider02.json

5.模拟消费者调用生产者,发起契约测试

首先切换到契约文件目录,运行生产者服务,发起如下指令:

 pact-verifier --provider-base-url=http://localhost:8000 --pact-url=consumer01-provider01.json

6.测试结果验证

感兴趣的小伙伴,可以拉一下我的测试代码仓库,本地run一下,加深对契约测试的印象。

代码仓库地址:gitee.com/qualityassu…

官方介绍地址:docs.pact.io/implementat…

- END -​