契約測(cè)試

背景

在當(dāng)前微服務(wù)和前后端分離大行其道的行業(yè)背景下等孵,越來越多的團(tuán)隊(duì)采用了前后端分離和微服務(wù)的架構(gòu)風(fēng)格去枷。
該服務(wù)架構(gòu)下會(huì)讓各個(gè)服務(wù)之間更多的依賴關(guān)系汁政,而且通常每個(gè)服務(wù)都是獨(dú)立的團(tuán)隊(duì)維護(hù),服務(wù)和服務(wù)之間大多通過API調(diào)用磷蜀。那么這種情況下可能就會(huì)出現(xiàn)一個(gè)問題餐曹,想象一下以下的場(chǎng)景:

A團(tuán)隊(duì)開發(fā)某服務(wù)并提供對(duì)應(yīng)API服務(wù)于颖,B團(tuán)隊(duì)是A團(tuán)隊(duì)的使用者調(diào)用A團(tuán)隊(duì)的API柏锄。


image.png

A團(tuán)隊(duì)埋頭苦干坪仇,B團(tuán)隊(duì)也爭(zhēng)分奪秒杂腰,兩邊都開發(fā)完了,往往一聯(lián)調(diào)椅文,就出現(xiàn)下圖這情況喂很。


image.png

為了保證API調(diào)用的準(zhǔn)確性, 我們需要對(duì)API進(jìn)行測(cè)試皆刺。但是即使這次測(cè)試通過了少辣,可能會(huì)隨著迭代的演進(jìn),重構(gòu)等羡蛾,A團(tuán)隊(duì)可能無形中修改了原API漓帅,那么這就可能會(huì)讓B團(tuán)隊(duì)在不知情的情況下服務(wù)不可用。

對(duì)測(cè)試而言也可能因?yàn)锳團(tuán)隊(duì)未完成對(duì)應(yīng)API服務(wù)或者A服務(wù)不穩(wěn)定痴怨,而照成測(cè)試無法介入B團(tuán)隊(duì)的測(cè)試或者測(cè)試效率低下忙干。

當(dāng)然你可能為了早點(diǎn)讓B團(tuán)隊(duì)可測(cè),你可能構(gòu)建測(cè)試替身浪藻,例如使用MockService捐迫,構(gòu)建A團(tuán)隊(duì)的服務(wù)替身“看上去貌似可以測(cè)試提前又可以避免A服務(wù)的不穩(wěn)定性施戴,但是問題又來了,即使Mock測(cè)試通過了钧惧,你能確定AB團(tuán)隊(duì)聯(lián)調(diào)就能通過暇韧?也許A團(tuán)隊(duì)返回的根本不是你Mock類型的數(shù)據(jù)或者說A團(tuán)隊(duì)的服務(wù)發(fā)生了變化。


image.png

問題和困境

  • API調(diào)用方對(duì)API提供方的變更經(jīng)常需要通過對(duì)API的測(cè)試來感知浓瞪。
  • 直接依賴真實(shí)API的測(cè)試效果受限與API提供方的穩(wěn)定性和反應(yīng)速度懈玻。

解決方案

解決方式首先是依賴關(guān)系的解耦,去掉直接對(duì)外部API的依賴乾颁,而是內(nèi)部和外部系統(tǒng)都依賴于一個(gè)雙方共同認(rèn)可的約定—“契約”涂乌,并且約定內(nèi)容的變化會(huì)被及時(shí)感知;其次英岭,將系統(tǒng)之間的集成測(cè)試湾盒,轉(zhuǎn)換為由契約生成的單元測(cè)試,例如通過契約描述的內(nèi)容诅妹,構(gòu)建測(cè)試替身罚勾。這樣毅人,同時(shí)契約替代外部API成為信息變更的載體。


image.png

什么是契約測(cè)試

契約測(cè)試也叫消費(fèi)者驅(qū)動(dòng)測(cè)試尖殃。
兩個(gè)角色:消費(fèi)者(Consumer)和 生產(chǎn)者(Provider)
一個(gè)思想:需求驅(qū)動(dòng)(消費(fèi)者驅(qū)動(dòng))
契約文件:由Consumer端和Provider端共同定義的規(guī)范丈莺,包含API路徑,輸入送丰,輸出缔俄。通常由Consumber生成。
實(shí)現(xiàn)原理:Consumer 端提供一個(gè)類似“契約”的東西(如json 文件器躏,約定好request和response)交給Provider 端俐载,告訴Provider 有什么需求,然后Provider 根據(jù)這份“契約”去實(shí)現(xiàn)登失。

PACT demo 分享

PACT 工作原理

PACT 是契約測(cè)試其中一個(gè)主流框架遏佣,最早是ruby實(shí)現(xiàn),現(xiàn)支持大部分開發(fā)語言壁畸,下圖是PACT的工作原理圖

image.png

第一步:Consumer 寫一個(gè)對(duì)接口發(fā)送請(qǐng)求的單元測(cè)試贼急,在運(yùn)行這個(gè)單元測(cè)試的時(shí)候,Consumer 會(huì)向Pact發(fā)起一個(gè)請(qǐng)求捏萍,Pact會(huì)將服務(wù)提供者自動(dòng)用一個(gè)MockService代替返回,并生成Json格式的契約文件空闲。
第二步:在Provider端做契約驗(yàn)證測(cè)試令杈。將Provider服務(wù)啟動(dòng)起來以后,通過一些命令碴倾,Pact會(huì)自動(dòng)按照契約生成接口發(fā)起Provider服務(wù)調(diào)用請(qǐng)求逗噩,并驗(yàn)證Provider的接口響應(yīng)是否滿足契約中的預(yù)期。

所以可以看到這個(gè)過程中跌榔,在消費(fèi)者端不用啟動(dòng)Provider异雁,在服務(wù)提供端不用啟動(dòng)Consumer,卻完成了與集成測(cè)試類似的驗(yàn)證測(cè)試僧须。

Node.js 代碼實(shí)踐

  1. 創(chuàng)建一個(gè)Express項(xiàng)目
  2. 安裝Pact相關(guān)包
    npm install --save-dev pact
    3.創(chuàng)建test/consumer/consumer.js文件纲刀,編寫Consumer調(diào)用Provider方法,例子是再封裝成一個(gè)API:
server.use(bodyParser.json())
server.use(bodyParser.urlencoded({ extended: true }))

server.get('/1', function (req, res) {
    var reqOpts = {
        uri: `http://localhost:8081/user/1`,
        headers: { 'Accept': 'application/json' },
        json: true
    }

    console.log(`**** Triggering request to http://localhost:8081} ****`)

    request(reqOpts)
        .then(function (rep) {
            console.log(rep);
            console.log('**** Received response ****');
            res.send(rep)
        })
        .catch(function (err) {
            res.status(500).send(err)
        })
});

server.listen(8080, function () {
    console.log(`**** Consumer listening on 8080. Provider: http://localhost:8081} ****`)
})

  1. 創(chuàng)建test/consumer/consumer-test.js文件担平,主要用于生成pact的契約文件
    1)創(chuàng)建一個(gè)Pact對(duì)象示绊,其對(duì)象表示依賴的一個(gè)生產(chǎn)者
  // 1. 創(chuàng)建一個(gè)Pact對(duì)象,其表示依賴的一個(gè)生產(chǎn)者端
  const provider = pact({
    consumer: 'TodoApp',
    provider: 'TodoService',
    port: 8080,
    log: path.resolve(process.cwd(), 'logs', 'pact.log'),
    dir: path.resolve(process.cwd(), 'pacts'), // json文件生成位置
    logLevel: 'INFO',
    spec: 2
  });

  1. 需要調(diào)用provider.setup() 啟動(dòng)mock server來mock生產(chǎn)者服務(wù)

3)定義消費(fèi)者與生產(chǎn)者相互交互的內(nèi)容暂论,與前一步代碼結(jié)合后如下

      // 2. 啟動(dòng)mock server來mock生產(chǎn)者服務(wù)
      provider.setup()
        // 3. 定義消費(fèi)者與生產(chǎn)者相互交互的內(nèi)容
        .then(() => {
          provider.addInteraction({
            state: 'have a matched user',
            uponReceiving: 'a request for get user',
            withRequest: {
              method: 'GET',
              path: '/1'
            },
            willRespondWith: {
              status: 200,
              headers: { 'Content-Type': 'application/json; charset=utf-8' },
              body: {
                id: 1,
                name: 'God'
              }
            }
          })
        })
        .then(() => done())
      });

這里需要說明的是state, 這里的state代表的是生產(chǎn)者所處的狀態(tài)面褐,生產(chǎn)者可以根據(jù)不同的狀態(tài)初始化不同的資源。因而消費(fèi)者在不同狀態(tài)下發(fā)送同樣的請(qǐng)求取胎,生產(chǎn)者卻因?yàn)樽陨沓跏蓟Y源的不同可以返回不同的結(jié)果展哭。
4)測(cè)試代碼中需要有發(fā)送請(qǐng)求到mock的生產(chǎn)者服務(wù)

   // 4. 測(cè)試代碼中需要有邏輯請(qǐng)求mock的生產(chǎn)者服務(wù)
      it('should response with user with id and name', (done) => {
        request.get('http://localhost:8080/1')
          .then((response) => {
            const user = response.body;
            expect(user.name).to.equal('God');
            provider.verify();
            done();
          })
          .catch((e) => {
            console.log('error', e);
            done(e);
          });
      });

5 將契約寫到文件中,關(guān)閉mock的生產(chǎn)者端

 // 5. 將契約寫到文件中,關(guān)閉mock的生產(chǎn)者端
 after(() => {
   provider.finalize();
 });

這時(shí)你啟動(dòng)消費(fèi)者服務(wù)匪傍,并運(yùn)行該test坝咐,你將會(huì)在工程根目錄多出pacts文件夾,并發(fā)現(xiàn)生成了todoapp-todoservice.json文件析恢,這個(gè)就是契約文件,上面描述了請(qǐng)求的發(fā)起路徑方法墨坚,返回的headers body等:

{
  "consumer": {
    "name": "TodoApp"
  },
  "provider": {
    "name": "TodoService"
  },
  "interactions": [
    {
      "description": "a request for get user",
      "providerState": "have a matched user",
      "request": {
        "method": "GET",
        "path": "/1"
      },
      "response": {
        "status": 200,
        "headers": {
          "Content-Type": "application/json; charset=utf-8"
        },
        "body": {
          "id": 1,
          "name": "God"
        }
      }
    }
  ],
  "metadata": {
    "pactSpecification": {
      "version": "0.0.0"
    }
  }
}

生成了契約文件,也就意味著消費(fèi)者端已經(jīng)給出了具體的接口返回字段需求映挂,剩下的就是生產(chǎn)者端根據(jù)契約文件開發(fā)對(duì)應(yīng)接口泽篮。

生產(chǎn)者端測(cè)試

為了demo簡(jiǎn)單,這里面把provider和Consumer放一個(gè)工程柑船,實(shí)際工作通常是兩個(gè)工程甚至兩個(gè)不同開發(fā)語言的工程帽撑。 而且代碼直接編寫了一個(gè)返回固定值的API,并編寫了test用于測(cè)試生產(chǎn)者端是否滿足消費(fèi)者端鞍时。

  1. 創(chuàng)建test/provider/provider.js文件亏拉,新建一個(gè)/user/:id接口
server.use((req, res, next) => {
  res.header('Content-Type', 'application/json')
  next()
});

server.get('/user/:id', (req, res) => {
  res.end(JSON.stringify({
    id: 1,
    name: 'God'
  }));
});

server.listen(8081, () => {
  console.log('User Service listening on http://localhost:8081')
});
  1. 創(chuàng)建test/provider/ptest.js文件,用于測(cè)試生產(chǎn)者端是否滿足消費(fèi)者的需求
const verifier = require('pact').Verifier;
const path = require('path');

// 驗(yàn)證生產(chǎn)者滿足消費(fèi)者的需求
describe('Pact Verification', () => {
    it('should validate the expectations of Matching Service', () => {

        const opts = {
            providerBaseUrl: 'http://localhost:8081',
            providerGetUser: 'http://localhost:8081/user/:id',
            pactUrls: [path.resolve(process.cwd(), './pacts/todoapp-todoservice.json')]
            // pactUrls: ['http://sjqasystst04:8081/pacts/provider/TodoService/consumer/TodoApp/latest']
        }

        return verifier.verifyProvider(opts)
            .then(output => {
                console.log('Pact Verification Complete!');
                console.log(output)
            });
    });
});

其中opts中定義了生產(chǎn)端的URL,要測(cè)試的接口URL逆巍,以及使用的契約文件及塘,并使用
require('pact').Verifier.verifyProvider 來驗(yàn)證契約

  1. 啟動(dòng)生產(chǎn)者端,并運(yùn)行該test锐极,會(huì)發(fā)現(xiàn)測(cè)試是通過的笙僚。如果修改下生產(chǎn)者端的接口返回,再次啟動(dòng)生產(chǎn)者端并運(yùn)行該test會(huì)發(fā)現(xiàn)測(cè)試是不會(huì)通過灵再。

Pact Broker

消費(fèi)者端生成的契約文件肋层,如果沒有一個(gè)平臺(tái)管理,勢(shì)必會(huì)比較麻煩翎迁,而Pact Broker是契約的管理者(代理人)栋猖。它提供了:

  • 發(fā)布和獲取契約的接口
  • 服務(wù)之間的依賴關(guān)系
  • 契約的版本管理
    例如上面例子,當(dāng)我把契約發(fā)布到Pact Broker上后汪榔,生產(chǎn)者端可以修改pactUrls獲取對(duì)應(yīng)的契約文件如:
    pactUrls: ['http://sjqasystst04:8081/pacts/provider/TodoService/consumer/TodoApp/latest']

Pact Broker Docker服務(wù)搭建

1.新建docker-compose.yml蒲拉,如下:

db:
    image: postgres:9.4
    container_name: pactbrokerdb
    environment:
      - POSTGRES_USER=pact
      - POSTGRES_PASSWORD=test

web:
    image: dius/pact-broker
    container_name: pactbrokerweb
    ports: ["8081:80"]
    links: ["db"]
    environment:
      - PACT_BROKER_DATABASE_USERNAME=pact
      - PACT_BROKER_DATABASE_PASSWORD=test
      - PACT_BROKER_DATABASE_HOST=db
      - PACT_BROKER_DATABASE_NAME=pact

注意:新版是叫pact-broker 舊版是 pact_broker

  1. 消費(fèi)者端創(chuàng)建publishPacts.js來發(fā)布契約
pact.publishPacts({
  pactUrls: [path.join(process.cwd(), 'pacts')], // 契約路徑
  pactBroker: 'http://sjqasystst04:8081', // pact broker 服務(wù)地址
  consumerVersion: '1.0.0'
});
  1. 運(yùn)行該文件,這時(shí)你就可以到pact broker上看到契約


    所有契約

    圖形化的關(guān)系

    API文檔

    API文檔上的黑色體字有沒發(fā)現(xiàn)都是消費(fèi)者端生成時(shí)設(shè)置的揍异。

靈活匹配

消費(fèi)者端制定的契約中每個(gè)字段不一定需要完全匹配全陨,其也支持正則匹配、類型匹配衷掷、數(shù)組匹配辱姨。

正則匹配

'gender': term({
        matcher: 'F|M',
        generate: 'F'
      })

類型匹配

body: {
      id: like(1),
      name: like('Billy')
    }

數(shù)組匹配

'users': eachLike({
    name: like('God')
  }, {
    min: 2
  });

min的默認(rèn)值是1, 這里表示至少有2個(gè)user戚嗅。

小結(jié)

契約測(cè)試幫我們解決了上面問題雨涛?

  • 可以使得消費(fèi)端和提供端之間測(cè)試解耦枢舶,不再需要客戶端和服務(wù)端聯(lián)調(diào)才能發(fā)現(xiàn)問題
  • 完全由消費(fèi)者驅(qū)動(dòng)的方式,消費(fèi)者需要什么數(shù)據(jù)替久,服務(wù)端就給什么樣的數(shù)據(jù)凉泄,數(shù)據(jù)契約也是由消費(fèi)者來定的
  • 測(cè)試前移,越早的發(fā)現(xiàn)問題蚯根,保證后續(xù)測(cè)試的完整性
  • 通過契約測(cè)試后众,團(tuán)隊(duì)能以一種離線的方式(不需要消費(fèi)者、提供者同時(shí)在線)颅拦,通過契約作為中間的標(biāo)準(zhǔn)蒂誉,驗(yàn)證提供者提供的內(nèi)容是否滿足消費(fèi)者的期望。

資料參考

JS測(cè)試之Pact測(cè)試
聊一聊契約測(cè)試
nodejs pact

本文源碼

https://github.com/MeYoung/pact_demo

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末距帅,一起剝皮案震驚了整個(gè)濱河市右锨,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌碌秸,老刑警劉巖绍移,帶你破解...
    沈念sama閱讀 206,968評(píng)論 6 482
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異讥电,居然都是意外死亡蹂窖,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,601評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門允趟,熙熙樓的掌柜王于貴愁眉苦臉地迎上來恼策,“玉大人,你說我怎么就攤上這事潮剪。” “怎么了分唾?”我有些...
    開封第一講書人閱讀 153,220評(píng)論 0 344
  • 文/不壞的土叔 我叫張陵抗碰,是天一觀的道長。 經(jīng)常有香客問我绽乔,道長弧蝇,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 55,416評(píng)論 1 279
  • 正文 為了忘掉前任折砸,我火速辦了婚禮看疗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘睦授。我一直安慰自己两芳,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,425評(píng)論 5 374
  • 文/花漫 我一把揭開白布去枷。 她就那樣靜靜地躺著怖辆,像睡著了一般是复。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上竖螃,一...
    開封第一講書人閱讀 49,144評(píng)論 1 285
  • 那天淑廊,我揣著相機(jī)與錄音,去河邊找鬼特咆。 笑死季惩,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的腻格。 我是一名探鬼主播画拾,決...
    沈念sama閱讀 38,432評(píng)論 3 401
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼荒叶!你這毒婦竟也來了碾阁?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 37,088評(píng)論 0 261
  • 序言:老撾萬榮一對(duì)情侶失蹤些楣,失蹤者是張志新(化名)和其女友劉穎脂凶,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體愁茁,經(jīng)...
    沈念sama閱讀 43,586評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡蚕钦,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,028評(píng)論 2 325
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了鹅很。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片嘶居。...
    茶點(diǎn)故事閱讀 38,137評(píng)論 1 334
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖促煮,靈堂內(nèi)的尸體忽然破棺而出邮屁,到底是詐尸還是另有隱情,我是刑警寧澤菠齿,帶...
    沈念sama閱讀 33,783評(píng)論 4 324
  • 正文 年R本政府宣布佑吝,位于F島的核電站,受9級(jí)特大地震影響绳匀,放射性物質(zhì)發(fā)生泄漏芋忿。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,343評(píng)論 3 307
  • 文/蒙蒙 一疾棵、第九天 我趴在偏房一處隱蔽的房頂上張望戈钢。 院中可真熱鬧,春花似錦是尔、人聲如沸殉了。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,333評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽宣渗。三九已至抖所,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間痕囱,已是汗流浹背田轧。 一陣腳步聲響...
    開封第一講書人閱讀 31,559評(píng)論 1 262
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留鞍恢,地道東北人傻粘。 一個(gè)月前我還...
    沈念sama閱讀 45,595評(píng)論 2 355
  • 正文 我出身青樓,卻偏偏與公主長得像帮掉,于是被迫代替她去往敵國和親弦悉。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,901評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容

  • 如今蟆炊,契約測(cè)試已經(jīng)逐漸成為測(cè)試圈中一個(gè)炙手可熱的話題稽莉,特別是在微服務(wù)大行其道的行業(yè)背景下,越來越多的團(tuán)隊(duì)開始關(guān)注服...
    ariman閱讀 12,480評(píng)論 28 33
  • 什么是契約 如果從契約產(chǎn)生的階段來說涩搓,現(xiàn)有資料表明最早要追溯到西周時(shí)期的《周恭王三年裘衛(wèi)典田契》污秆,將契約文字刻寫在...
    ThoughtWorks閱讀 2,928評(píng)論 1 12
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)昧甘,斷路器良拼,智...
    卡卡羅2017閱讀 134,601評(píng)論 18 139
  • 正如大家所知庸推,最初QA都是手動(dòng)執(zhí)行測(cè)試用例,開發(fā)人員每修改一個(gè)版本浇冰,QA就要手動(dòng)測(cè)試一遍贬媒,隨著功能的不斷增加,手動(dòng)...
    ThoughtWorks閱讀 2,820評(píng)論 1 18
  • 前言 本文源于一次關(guān)于處理契約測(cè)試在CI上更新的小討論肘习。僅拋磚引玉掖蛤,期待更多地討論和建議。 上下文鋪墊 契約測(cè)試多...
    MoZhou閱讀 598評(píng)論 1 3