淺談mock

santree發表於2019-03-02

閱讀之前

希望你能有以下基礎,方便閱讀:

  • ECMAScript 6 (ES6)

為什麼需要Mock

image
這樣的場景,相信大家會覺得似曾相識。

現今的業務系統已經很少是孤立存在的了,尤其對於一個大公司而言,各個部門之間的配合非常密切,我們或多或少都需要使用兄弟團隊或是其他公司提供的介面服務。這樣的話,就對我們的聯調和測試造成了很大的麻煩。假如各個兄弟部門的步伐完全一致,那麼問題就會少很多,但理想很豐滿,現實卻很骨感,要做到步伐一致基本是不可能的。

為此,我們就需要使用一些工具來幫助我們將業務單元之間儘量解耦,它就是Mock

什麼是Mock

如果將mock單獨翻譯過來,其意義為 “虛假、虛設”,因此在軟體開發領域,我們也可以將其理解成 “虛假資料”,或者 “真實資料的替身”

Mock的好處

  • 團隊可以更好地並行工作

當使用mock之後,各團隊之間可以不需要再互相等待對方的進度,只需要約定好相互之間的資料規範(文件),即可使用mock構建一個可用的介面,然後儘快的進行開發和除錯以及自測,提升開發進度的的同時,也將發現缺陷的時間點大大提前。

  • 開啟TDD(Test-Driven Development)模式,即測試驅動開發

單元測試是TDD實現的基石,而TDD經常會碰到協同模組尚未開發完成的情況,但是有了mock,這些一切都不是問題。當介面定義好後,測試人員就可以建立一個Mock,把介面新增到自動化測試環境,提前建立測試。

  • 測試覆蓋率

比如一個介面在各種不同的狀態下要返回不同的值,之前我們的做法是復現這種狀態然後再去請求介面,這是非常不科學的做法,而且這種復現方法很大可能性因為操作的時機或者操作方式不當導致失敗,甚至汙染之前資料庫中的資料。如果我們使用mock,就完全不用擔心這些問題。

  • 方便演示

通過使用Mock模擬資料介面,我們即可在只開發了UI的情況下,無須服務端的開發就可以進行產品的演示。

  • 隔離系統

在使用某些介面的時候,為了避免系統中資料庫被汙染,我們可以將這些介面調整為Mock的模式,以此保證資料庫的乾淨。

在吹了這麼多的Mock之後,相信大家一定躍躍欲試了,那麼接下來我們談一談實現Mock的幾種方法。

實現Mock

“倔強青銅”

好了,我們先從最倔強的“青銅”開始吧,在沒有mock的時候,我們是如何在沒有真實介面的情況下進行開發的呢?

在本人的記憶裡,當遇到這種情況,我最開始的做法就是將資料先寫死在業務中,比如:

// api
import api from '../api/index';

function getApiMessage() {
    return new Promise((resolve) => {
        resolve({
            message: '請求成功'
        });
    })
    // return api.getApiMessage();
}
複製程式碼

我會將真實的請求註釋掉,return一個resolve假資料的promise代替真實的請求,然後我在呼叫這個方法的時候就會返回一個resolve我自己定義的虛假資料的promise而不是從尚未完成的介面獲得的promise。看起來還不錯,起碼我能夠在沒有介面的情況下繼續進行開發了。雖然當遇到複雜的列表資料的時候,自己寫起來有點手疼。

但是虛假資料和業務如此耦合真的好嗎?假如當真正的介面完成之後,因為業務可以“正確執行”而忘記了移除這些虛假資料,導致實際你使用的資料一直是你自己編造而非真實的,那可是相當嚴重的問題。所以我們接下來需要思考的便是如何儘量的減少在業務程式碼中寫入這些虛假資料。為了達成這個目標,讓我們正式晉級mock的“榮耀黃金”段位。

“榮耀黃金”

在mock的“榮耀黃金”段位,我們擁有了一個非常好用的工具:mockJs,通過使用mockJs我們能根據模板和規則生成複雜的介面資料,而無需我們自己動手去書寫,例如:

// api
import api from '../api/index';
import Mock from 'mockjs';

function getApiMessage() {
    return new Promise((resolve) => {
        resolve(Mock.mock({
            list|1-20: ['mock資料']
        });
    })
    // return api.getApiMessage();
}
/**
* 通過 Mock.mock 方法和 list|1-20: ['mock資料'] 模板
* 我們將生成一個長度為 1-20, 每個值都為 'mock資料' 陣列
*/
複製程式碼

但是這樣做始終只不過是方便了我們“造假”而已,並不能將“假貨”真的從我們的業務程式碼中移除出去。為了實現這個目的,我們不妨先來分析我們的需求:

  • 模擬資料與業務程式碼完全分離
  • 通過一些配置,達到只mock部分資料,大部分的資料還是從請求中獲取

首先,如果我們要想要模擬資料和業務程式碼完全分離,我們必須要想辦法在請求的時候做一些文章,讓其在請求的時候去獲取mock資料而非去請求真正的介面,也就是所謂的“請求攔截”,而實現請求攔截也同樣有兩種方式:

  • 修改請求連結到mock-server,在mock-server配置mock資料和路由
// api/index.js
// 通過新增getDataUseMock方法來說明使用了mock方法
import request from '../request';

function getDataUseMock(data) {
    request({
        mock: true
    });
}
// request/index.js
const mockServer = 'http://127.0.0.1:8081';

function request(opt) {
    if (opt.mock) {
        const apiName = opt.api;
        opt.url = `${mockServer}/${apiName}`;
    }
    ...
}
複製程式碼
  • 直接在檢測使用mock時,從mock資料檔案中取出對應key值的資料
// api/index.js
// 通過新增getDataUseMock方法來說明使用了mock方法
import request from '../request';

function getDataUseMock(data) {
    request({
        mock: true
    });
}

// request/index.js
import mockData from 'mock/db.js';

function request(opt) {
    if (opt.mock) {
        const apiName = opt.api;
        return new Promise((resolve) => {
            resolve(mockData.apiName)
        })
    }
    ...
}

//mock/db.js
export default {
    '/api/test': {
        msg: '請求成功'
    }
}
複製程式碼

乍一看好像第二種方式似乎更簡單,事實也確實如此,但是考慮到如果我是直接從檔案中直接讀取資料,那麼業務上的行為也會改變,該發請求的地方並沒有發請求,所以我還是選擇了自己搭建一個本地的服務,通過控制路由返回不同的mock資料來處理,並且通過為請求增加一個額外mock引數通知業務哪些介面應當被自建的mock-server攔截,從而儘量減少對原有業務的影響。

mock-server開發之前,我們需要明白我們的mock-server應當能做哪些事情:

  • 所改即所得,具有熱更新的能力,每次增加 /修改 mock 介面時不需要重啟 mock 服務,更不用重啟前端構建服務
  • mock 資料可以由工具生成不需要自己手動寫
  • 能模擬 POST、GET 請求

因為mock的模擬資料都在本地維護,我們所需要的只要是個無介面的能夠響應請求的server即可,所以我選擇了json-server

在構建server之前,我們先要明確我們需要模擬的資料是什麼,以及用什麼(mockjs)去維護

// db.js
var Mock = require('mockjs');

// 通過使用mock.js,來避免手寫資料
module.exports = {
  getComment: Mock.mock({
    "error": 0,
    "message": "success",
    "result|40": [{
      "author": "@name",
      "comment": "@cparagraph",
      "date": "@datetime"
    }]
  })
};
複製程式碼

其次我們要知道我們跳轉的訪問路由是哪些:

// routes.js
// 根據db.js中的key值,自動生成的路由便是/[key],在route.js中的宣告只是為了重定向
module.exports = {
  "/comment/get": "/getComment"
}
複製程式碼

然後我們就可以書寫我們啟動server的主要程式碼了:

// server.js
const jsonServer = require('json-server')
const db = require('./db.js')
const routes = require('./routes.js')
const port = 3000;

const server = jsonServer.create()
// 使用mock的資料生成對應的路由
const router = jsonServer.router(db)
const middlewares = jsonServer.defaults()
// 根據路由列表重寫路由
const rewriter = jsonServer.rewriter(routes)

server.use(middlewares)
// 將 POST 請求轉為 GET,滿足可以接受 POST 和 GET 請求的需求
server.use((request, res, next) => {
  request.method = 'GET';
  next();
})

server.use(rewriter) // 注意:rewriter 的設定一定要在 router 設定之前
server.use(router)

server.listen(port, () => {
  console.log('open mock server at localhost:' + port)
})
複製程式碼

由此,只要使用node server.js便能夠啟動一個mock-server了,但是這樣啟動的server,並不能因為我修改route.js或者db.js而實時更新,也就是說,我需要每次都重啟一次才能更新我的server,這裡還需要我們進行一個小操作,比如使用nodemon來監控我們的mock-server.

// 將所有和mock相關的檔案:db.js route.js server.js 放入mock資料夾
// 然後執行:
$ nodemon --watch mock mock/server.js

// 就能夠啟動一個能自動熱更新的mock-server了。
複製程式碼

這之後,我們只需要在自己的業務程式碼中,使用我們之前定義的類似於getDataUseMock的方法,就可以對指定API進行mock啦。

雖然我們這樣做已經完成了mock資料和業務程式碼的完全分離,但是還是不可避免的在業務程式碼中使用了特殊的方法來宣告我需要mock某個介面,還是同樣要面對當不需要mock時,要刪除這些方法並替換成正式請求的方法的問題。而且mock資料的部分仍然放在和業務程式碼一個git目錄下,只有開發者才有許可權去修改和增加,並沒有很好地達到mock應當有的作用。

為此,我徵求了部門Leader和“廣大”開發者的意見,確定了我們需要的mock應當是怎樣的:

  • 儘量少的修改業務中的程式碼就能使用mock
  • 修改的業務程式碼不會影響正常的業務流程
  • mock-server 應當是面向所有人,而不只是前端開發者
  • 能夠視覺化的修改和增加 mock 介面和 mock 資料
  • 能夠同時支援多個專案使用

在這幾個基本原則的幫助下,我們的mock終於晉級到了“永恆鑽石”段位。

“永恆鑽石”

在鑽石段位的加持下,我找到了 mock-server 的“上分利器”: 來自阿里前端團隊開源的THX工具庫中的RAP2,其包含的優勢完全符合我對mock的需求。在依照網上的教程,將RAP2部署到了我們本地的伺服器上之後,我們只需要通過在本地配置 hosts 檔案即可訪問我們自己的RAP2,這之後,我們需要做的僅僅只剩下業務程式碼中的處理了:

  • 儘量少的修改業務中的程式碼就能使用mock
  • 修改的業務程式碼不會影響正常的業務流程

為了能夠儘量少的去修改程式碼並且讓修改的程式碼不影響正常的業務流程,我們需要增加一個特殊的開發模式,僅在這個開發模式下,我們修改的程式碼才會生效,或者說才會存在。

我們給我們新增的開發模式可以命名為mock開發模式,為了區分這個開發模式,我們使用nodejs中的環境變數來進行區分。

"scripts": {
    "dev:mock": "cross-env MOCK=true npm run dev"
}
複製程式碼

在使用cross-env宣告瞭環境變數之後,我們可以通過process.env.MOCK獲取到我們宣告的環境變數的值,當我們增加的MOCK變數存在,且為true時,我們才進行mock的請求攔截。

但是我們僅僅宣告這一點還是不夠,我們還需要通知業務程式碼,哪些介面需要被mock。所以,我們還需要一個mock模式下才會存在的列表,來告訴我們哪些介面應當被mock。

// config.js
if (process.env.MOCK) {
    config.mockList = [
        '/api/test',
        '/api/needMock'
    ]
} else {
    config.mockList = [];
}
複製程式碼

當然你也可以使用條件編譯來判斷是否將config.mockList打入你的程式碼裡,這是更加好的選擇。

接下來,你只需要在你封裝的請求方法裡,對config的mockList和你當前請求的api進行對比,判斷其是否要進行mock即可。

import config from '../config/config';

const mockServer = 'http://rap2.xxx.com'

function request(opt) {
    const apiName = opt.api;
    if (config.mockList && config.mockList.includes(apiName)) {
        opt.url = `${mockServer}/${apiName}`;
    }
    ...
}
複製程式碼

如此,我們的mock終於到達了最終形態,從此只要介面文件(甚至RAP2的mock介面就可以直接作為介面文件),我們就能隨意的進行開發測試啦~

RAP2的使用

從團隊開始

團隊是倉庫的上級單位,一個團隊可以擁有多個mock倉庫,但是不是隻有團隊才能擁有倉庫,個人也可以。使用團隊的目的只是為了讓團隊下的倉庫不被團隊外人員獲悉,保持一個團隊的私密性(當然你也可以選擇公開團隊)。

倉庫

倉庫是介面的上級單位,可以歸屬於個人或者團隊,每個倉庫都可以指派開發人員,被指定的人員可以修改或者新增倉庫的介面,未被指派的人員僅能檢視介面,每個倉庫都擁有一個特定的倉庫域名字首。其下的介面域名規則都遵循:${倉庫字首域名}${介面配置域名},且每個倉庫都提供一個介面獲取當前倉庫資料。

介面

我們先來看看介面配置頁面的組成:

image
可以看到介面頁面主要由如下部分組成:

  • 新建介面(介面列表)
  • 介面模組
  • 介面詳情(請求引數和響應引數)

在介面詳情中,請求的mock介面的路由是在新建介面的時候去建立的,建立之後自動生成一個介面,請求地址就是${倉庫域名}${介面路由}

請求引數的部分配置我們最主要要關注的是生成規則和預設值,其規則和模板可以參考mockJs文件中的語法規範,生成規則遵循資料模板定義規範(Data Template Definition,DTD),預設值遵循資料佔位符定義規範(Data Placeholder Definition,DPD)

引用內容