背景
最近剛換城市,忙于找工作,趁著等待面試結(jié)果的間隙給自己充充電袁波。把之前一直很好奇的“微服務(wù)測試”學(xué)習(xí)了一番导街,了解了一個(gè)新的概念----契約測試披泪。
接口的契約
對(duì)于一個(gè)api接口,一般會(huì)有接口文檔說明搬瑰。文檔中會(huì)規(guī)定如何調(diào)用該接口款票、如何傳參控硼,以及接口會(huì)如何響應(yīng)。其實(shí)這個(gè)就可以理解為接口的"契約"艾少。前端按照這個(gè)“契約”去調(diào)用卡乾,服務(wù)端按照“契約”返回響應(yīng)的內(nèi)容。若服務(wù)端擅自修改了返回內(nèi)容結(jié)構(gòu)缚够,就算作違反"契約"幔妨,也是造成bug的一個(gè)原因。
擴(kuò)展到微服務(wù)當(dāng)中谍椅,每個(gè)服務(wù)與服務(wù)之間的調(diào)用误堡,同樣需要遵守這種"契約",才能保證接口功能的穩(wěn)定性雏吭。
什么是契約測試锁施?
微服務(wù)架構(gòu)中,一般分為“提供者”(provider思恐,提供接口的服務(wù)) 和“消費(fèi)者”(consumer沾谜,調(diào)用接口的服務(wù))
接口文檔可以稱之為對(duì)接口契約的具體描述,主要提供給人看胀莹。而契約測試基跑,則是將契約具象為代碼/工具可識(shí)別的形式,比如json描焰、yml媳否、DSL格式。然后借助相關(guān)測試工具荆秦,根據(jù)這份契約篱竭,自動(dòng)測試“消費(fèi)者/提供者”接口是否正常。
契約測試一般分兩種步绸,一種是消費(fèi)者驅(qū)動(dòng)掺逼,一種是提供者驅(qū)動(dòng)。其中最常用的瓤介,是消費(fèi)者驅(qū)動(dòng)的契約測試(簡稱 CDC)吕喘。即由“消費(fèi)者”定義出接口“契約”,然后測試“提供者”的接口是否符合契約刑桑。
契約測試工具使用--PACT
契約測試工具貌似有不少氯质,此處介紹一個(gè)常用工具--- PACT。
pact是一個(gè)契約測試框架祠斧,目前支持java闻察、python、ruby等多種語言。
pact契約測試分為兩步:
- 編寫test用例辕漂,生成契約文件(不需要啟動(dòng)服務(wù))呢灶。
- 利用pact-verifier命令和契約文件,驗(yàn)證接口提供者是否正確 (需要啟動(dòng)提供者服務(wù))
以下為demo示例:
1钉嘹、服務(wù)A填抬,提供者
import json
from flask import Flask
app = Flask(__name__)
@app.route('/')
def get_info():
info = {
"name": "zhangsan",
"age": 20
}
return json.dumps(info)
if __name__ == '__main__':
app.run(port=8080)
2、服務(wù)B隧期,消費(fèi)者。(調(diào)用服務(wù)A的接口)
import json
import requests
from flask import Flask
app = Flask(__name__)
@app.route('/')
def show_info():
res = requests.get("http://localhost:8080/").json()
result = {
"code":0,
"msg":"ok",
"data": res
}
return json.dumps(result)
if __name__ == '__main__':
app.run(port=8081)
3赘娄、測試用例仆潮,用于生成契約文件
import atexit
import requests
import unittest
from pact.consumer import Consumer
from pact.provider import Provider
# 定義一個(gè)pact,消費(fèi)者是ModuleB遣臼,生產(chǎn)者是ModuleA性置,契約文件存放在pacts文件夾下
pact = Consumer('ModuleB').has_pact_with(Provider('ModuleA'), pact_dir='./pacts')
# 啟動(dòng)服務(wù)
pact.start_service()
atexit.register(pact.stop_service)
# 測試用例
class UserTesting(unittest.TestCase):
def test_service(self):
# 消費(fèi)者定義的期望結(jié)果
expected = {"name": "zhangsan", "age": 20}
# 消費(fèi)者定義的契約的實(shí)際內(nèi)容。包括請(qǐng)求參數(shù)揍堰、請(qǐng)求方法鹏浅、請(qǐng)求頭、響應(yīng)值等
(pact
.given('test service.')
.upon_receiving('a request for serviceB')
.with_request('get', '/')
.will_respond_with(200, body=expected))
# pact自帶一個(gè)mock服務(wù)屏歹,端口 1234
# 用requests向mock接口發(fā)送請(qǐng)求隐砸,驗(yàn)證mock的結(jié)果是否正確
with pact:
res = requests.get("http://localhost:1234").json()
self.assertEqual(res, expected)
if __name__ == "__main__":
ut = UserTesting()
ut.test_service()
4、實(shí)際生成的契約文件 moduleb-modulea.json
{
"consumer": {
"name": "ModuleB"
},
"provider": {
"name": "ModuleA"
},
"interactions": [
{
"description": "a request for serviceB",
"providerState": "test service.",
"request": {
"method": "get",
"path": "/"
},
"response": {
"status": 200,
"headers": {
},
"body": {
"name": "zhangsan",
"age": 20
}
}
}
],
"metadata": {
"pactSpecification": {
"version": "2.0.0"
}
}
}
5蝙眶、根據(jù)契約文件季希,驗(yàn)證服務(wù)A是否正確
a. 啟動(dòng)服務(wù)A(提供者)
b. 執(zhí)行pact-verifier --provider-base-url=http://127.0.0.1:8080 --pact-url=./pacts/moduleb-modulea.json
,即可驗(yàn)證接口是否符合契約
契約測試的優(yōu)點(diǎn)
- 在開發(fā)接口前定義好契約幽纷,消費(fèi)者和提供者可以并行開發(fā)式塌。根據(jù)契約可以很容易自測,不必等到聯(lián)調(diào)時(shí)才暴露問題
- 確保變動(dòng)的安全性和準(zhǔn)確性友浸。只要有變化峰尝,契約測試即可第一時(shí)間發(fā)現(xiàn)
- 契約文件可以直觀追蹤接口的變化
- 可以將契約測試集成到CI中
- 每次只測試單個(gè)服務(wù),更容易定位問題
- 來自于模擬服務(wù)的可靠響應(yīng)能夠降低測試的不穩(wěn)定性
契約測試與接口測試的區(qū)別
契約測試與接口測試的原理都是一樣的收恢,都是發(fā)送請(qǐng)求武学、驗(yàn)證響應(yīng)結(jié)果。但是接口測試更多的關(guān)注業(yè)務(wù)api的功能派诬、邏輯劳淆,而契約測試主要關(guān)注接口是否符合“契約”,監(jiān)控接口的變動(dòng)默赂。
契約測試相當(dāng)于將測試工作前移沛鸵,在開發(fā)階段就能自測。并且契約測試更加輕量級(jí)。