閱讀之前
希望你能有以下基礎(chǔ)抚官,方便閱讀:
- ECMAScript 6 (ES6)
為什么需要Mock
這樣的場景上陕,相信大家會覺得似曾相識桩砰。
現(xiàn)今的業(yè)務(wù)系統(tǒng)已經(jīng)很少是孤立存在的了,尤其對于一個大公司而言释簿,各個部門之間的配合非常密切亚隅,我們或多或少都需要使用兄弟團隊或是其他公司提供的接口服務(wù)。這樣的話庶溶,就對我們的聯(lián)調(diào)和測試造成了很大的麻煩煮纵。假如各個兄弟部門的步伐完全一致懂鸵,那么問題就會少很多,但理想很豐滿行疏,現(xiàn)實卻很骨感匆光,要做到步伐一致基本是不可能的。
為此酿联,我們就需要使用一些工具來幫助我們將業(yè)務(wù)單元之間盡量解耦终息,它就是Mock
什么是Mock
如果將mock單獨翻譯過來,其意義為 “虛假贞让、虛設(shè)”周崭,因此在軟件開發(fā)領(lǐng)域,我們也可以將其理解成 “虛假數(shù)據(jù)”震桶,或者 “真實數(shù)據(jù)的替身”休傍。
Mock的好處
- 團隊可以更好地并行工作
當使用mock之后征绎,各團隊之間可以不需要再互相等待對方的進度蹲姐,只需要約定好相互之間的數(shù)據(jù)規(guī)范(文檔),即可使用mock構(gòu)建一個可用的接口人柿,然后盡快的進行開發(fā)和調(diào)試以及自測柴墩,提升開發(fā)進度的的同時,也將發(fā)現(xiàn)缺陷的時間點大大提前凫岖。
- 開啟TDD(Test-Driven Development)模式江咳,即測試驅(qū)動開發(fā)
單元測試是TDD實現(xiàn)的基石,而TDD經(jīng)常會碰到協(xié)同模塊尚未開發(fā)完成的情況哥放,但是有了mock歼指,這些一切都不是問題。當接口定義好后甥雕,測試人員就可以創(chuàng)建一個Mock踩身,把接口添加到自動化測試環(huán)境,提前創(chuàng)建測試社露。
- 測試覆蓋率
比如一個接口在各種不同的狀態(tài)下要返回不同的值挟阻,之前我們的做法是復(fù)現(xiàn)這種狀態(tài)然后再去請求接口,這是非常不科學的做法峭弟,而且這種復(fù)現(xiàn)方法很大可能性因為操作的時機或者操作方式不當導(dǎo)致失敗附鸽,甚至污染之前數(shù)據(jù)庫中的數(shù)據(jù)。如果我們使用mock瞒瘸,就完全不用擔心這些問題坷备。
- 方便演示
通過使用Mock模擬數(shù)據(jù)接口,我們即可在只開發(fā)了UI的情況下情臭,無須服務(wù)端的開發(fā)就可以進行產(chǎn)品的演示击你。
- 隔離系統(tǒng)
在使用某些接口的時候玉组,為了避免系統(tǒng)中數(shù)據(jù)庫被污染,我們可以將這些接口調(diào)整為Mock的模式丁侄,以此保證數(shù)據(jù)庫的干凈惯雳。
在吹了這么多的Mock之后,相信大家一定躍躍欲試了鸿摇,那么接下來我們談一談實現(xiàn)Mock的幾種方法石景。
實現(xiàn)Mock
“倔強青銅”
好了,我們先從最倔強的“青銅”開始吧拙吉,在沒有mock的時候潮孽,我們是如何在沒有真實接口的情況下進行開發(fā)的呢?
在本人的記憶里筷黔,當遇到這種情況往史,我最開始的做法就是將數(shù)據(jù)先寫死在業(yè)務(wù)中,比如:
// api
import api from '../api/index';
function getApiMessage() {
return new Promise((resolve) => {
resolve({
message: '請求成功'
});
})
// return api.getApiMessage();
}
我會將真實的請求注釋掉佛舱,return
一個resolve
假數(shù)據(jù)的promise
代替真實的請求椎例,然后我在調(diào)用這個方法的時候就會返回一個resolve
我自己定義的虛假數(shù)據(jù)的promise
而不是從尚未完成的接口獲得的promise
∏胱妫看起來還不錯订歪,起碼我能夠在沒有接口的情況下繼續(xù)進行開發(fā)了。雖然當遇到復(fù)雜的列表數(shù)據(jù)的時候肆捕,自己寫起來有點手疼刷晋。
但是虛假數(shù)據(jù)和業(yè)務(wù)如此耦合真的好嗎?假如當真正的接口完成之后慎陵,因為業(yè)務(wù)可以“正確運行”而忘記了移除這些虛假數(shù)據(jù)眼虱,導(dǎo)致實際你使用的數(shù)據(jù)一直是你自己編造而非真實的,那可是相當嚴重的問題席纽。所以我們接下來需要思考的便是如何盡量的減少在業(yè)務(wù)代碼中寫入這些虛假數(shù)據(jù)捏悬。為了達成這個目標,讓我們正式晉級mock的“榮耀黃金”段位胆筒。
“榮耀黃金”
在mock的“榮耀黃金”段位邮破,我們擁有了一個非常好用的工具:mockJs,通過使用mockJs我們能根據(jù)模板和規(guī)則生成復(fù)雜的接口數(shù)據(jù)仆救,而無需我們自己動手去書寫抒和,例如:
// api
import api from '../api/index';
import Mock from 'mockjs';
function getApiMessage() {
return new Promise((resolve) => {
resolve(Mock.mock({
list|1-20: ['mock數(shù)據(jù)']
});
})
// return api.getApiMessage();
}
/**
* 通過 Mock.mock 方法和 list|1-20: ['mock數(shù)據(jù)'] 模板
* 我們將生成一個長度為 1-20, 每個值都為 'mock數(shù)據(jù)' 數(shù)組
*/
但是這樣做始終只不過是方便了我們“造假”而已,并不能將“假貨”真的從我們的業(yè)務(wù)代碼中移除出去彤蔽。為了實現(xiàn)這個目的摧莽,我們不妨先來分析我們的需求:
- 模擬數(shù)據(jù)與業(yè)務(wù)代碼完全分離
- 通過一些配置,達到只mock部分數(shù)據(jù)顿痪,大部分的數(shù)據(jù)還是從請求中獲取
首先镊辕,如果我們要想要模擬數(shù)據(jù)和業(yè)務(wù)代碼完全分離油够,我們必須要想辦法在請求的時候做一些文章,讓其在請求的時候去獲取mock數(shù)據(jù)而非去請求真正的接口征懈,也就是所謂的“請求攔截”石咬,而實現(xiàn)請求攔截也同樣有兩種方式:
- 修改請求鏈接到mock-server,在mock-server配置mock數(shù)據(jù)和路由
// 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數(shù)據(jù)文件中取出對應(yīng)key值的數(shù)據(jù)
// 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: '請求成功'
}
}
乍一看好像第二種方式似乎更簡單鬼悠,事實也確實如此,但是考慮到如果我是直接從文件中直接讀取數(shù)據(jù)亏娜,那么業(yè)務(wù)上的行為也會改變焕窝,該發(fā)請求的地方并沒有發(fā)請求,所以我還是選擇了自己搭建一個本地的服務(wù)维贺,通過控制路由返回不同的mock數(shù)據(jù)來處理它掂,并且通過為請求增加一個額外mock
參數(shù)通知業(yè)務(wù)哪些接口應(yīng)當被自建的mock-server
攔截,從而盡量減少對原有業(yè)務(wù)的影響溯泣。
在mock-server
開發(fā)之前虐秋,我們需要明白我們的mock-server
應(yīng)當能做哪些事情:
- 所改即所得,具有熱更新的能力发乔,每次增加 /修改 mock 接口時不需要重啟 mock 服務(wù)熟妓,更不用重啟前端構(gòu)建服務(wù)
- mock 數(shù)據(jù)可以由工具生成不需要自己手動寫
- 能模擬 POST雪猪、GET 請求
因為mock的模擬數(shù)據(jù)都在本地維護栏尚,我們所需要的只要是個無界面的能夠響應(yīng)請求的server即可,所以我選擇了json-server
在構(gòu)建server
之前只恨,我們先要明確我們需要模擬的數(shù)據(jù)是什么译仗,以及用什么(mockjs)去維護
// db.js
var Mock = require('mockjs');
// 通過使用mock.js,來避免手寫數(shù)據(jù)
module.exports = {
getComment: Mock.mock({
"error": 0,
"message": "success",
"result|40": [{
"author": "@name",
"comment": "@cparagraph",
"date": "@datetime"
}]
})
};
其次我們要知道我們跳轉(zhuǎn)的訪問路由是哪些:
// routes.js
// 根據(jù)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的數(shù)據(jù)生成對應(yīng)的路由
const router = jsonServer.router(db)
const middlewares = jsonServer.defaults()
// 根據(jù)路由列表重寫路由
const rewriter = jsonServer.rewriter(routes)
server.use(middlewares)
// 將 POST 請求轉(zhuǎn)為 GET,滿足可以接受 POST 和 GET 請求的需求
server.use((request, res, next) => {
request.method = 'GET';
next();
})
server.use(rewriter) // 注意:rewriter 的設(shè)置一定要在 router 設(shè)置之前
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
來監(jiān)控我們的mock-server
.
// 將所有和mock相關(guān)的文件:db.js route.js server.js 放入mock文件夾
// 然后執(zhí)行:
$ nodemon --watch mock mock/server.js
// 就能夠啟動一個能自動熱更新的mock-server了忱详。
這之后,我們只需要在自己的業(yè)務(wù)代碼中跺涤,使用我們之前定義的類似于getDataUseMock
的方法匈睁,就可以對指定API進行mock啦监透。
雖然我們這樣做已經(jīng)完成了mock數(shù)據(jù)和業(yè)務(wù)代碼的完全分離,但是還是不可避免的在業(yè)務(wù)代碼中使用了特殊的方法來聲明我需要mock某個接口航唆,還是同樣要面對當不需要mock時胀蛮,要刪除這些方法并替換成正式請求的方法的問題。而且mock數(shù)據(jù)的部分仍然放在和業(yè)務(wù)代碼一個git目錄下糯钙,只有開發(fā)者才有權(quán)限去修改和增加醇滥,并沒有很好地達到mock應(yīng)當有的作用。
為此超营,我征求了部門Leader和“廣大”開發(fā)者的意見鸳玩,確定了我們需要的mock應(yīng)當是怎樣的:
- 盡量少的修改業(yè)務(wù)中的代碼就能使用
mock
- 修改的業(yè)務(wù)代碼不會影響正常的業(yè)務(wù)流程
-
mock-server
應(yīng)當是面向所有人,而不只是前端開發(fā)者 - 能夠可視化的修改和增加
mock
接口和mock
數(shù)據(jù) - 能夠同時支持多個項目使用
在這幾個基本原則的幫助下演闭,我們的mock終于晉級到了“永恒鉆石”段位不跟。
“永恒鉆石”
在鉆石段位的加持下,我找到了 mock-server
的“上分利器”: 來自阿里前端團隊開源的THX工具庫中的RAP2米碰,其包含的優(yōu)勢完全符合我對mock的需求窝革。在依照網(wǎng)上的教程,將RAP2
部署到了我們本地的服務(wù)器上之后吕座,我們只需要通過在本地配置 hosts
文件即可訪問我們自己的RAP2
虐译,這之后,我們需要做的僅僅只剩下業(yè)務(wù)代碼中的處理了:
- 盡量少的修改業(yè)務(wù)中的代碼就能使用
mock
- 修改的業(yè)務(wù)代碼不會影響正常的業(yè)務(wù)流程
為了能夠盡量少的去修改代碼并且讓修改的代碼不影響正常的業(yè)務(wù)流程吴趴,我們需要增加一個特殊的開發(fā)模式漆诽,僅在這個開發(fā)模式下,我們修改的代碼才會生效锣枝,或者說才會存在厢拭。
我們給我們新增的開發(fā)模式可以命名為mock開發(fā)模式,為了區(qū)分這個開發(fā)模式撇叁,我們使用nodejs中的環(huán)境變量來進行區(qū)分供鸠。
"scripts": {
"dev:mock": "cross-env MOCK=true npm run dev"
}
在使用cross-env
聲明了環(huán)境變量之后,我們可以通過process.env.MOCK
獲取到我們聲明的環(huán)境變量的值陨闹,當我們增加的MOCK
變量存在楞捂,且為true
時,我們才進行mock的請求攔截趋厉。
但是我們僅僅聲明這一點還是不夠寨闹,我們還需要通知業(yè)務(wù)代碼,哪些接口需要被mock觅廓。所以鼻忠,我們還需要一個mock模式下才會存在的列表,來告訴我們哪些接口應(yīng)當被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終于到達了最終形態(tài)哨啃,從此只要接口文檔(甚至RAP2的mock接口就可以直接作為接口文檔),我們就能隨意的進行開發(fā)測試啦~
RAP2的使用
從團隊開始
團隊是倉庫的上級單位写妥,一個團隊可以擁有多個mock倉庫拳球,但是不是只有團隊才能擁有倉庫,個人也可以珍特。使用團隊的目的只是為了讓團隊下的倉庫不被團隊外人員獲悉祝峻,保持一個團隊的私密性(當然你也可以選擇公開團隊)。
倉庫
倉庫是接口的上級單位扎筒,可以歸屬于個人或者團隊莱找,每個倉庫都可以指派開發(fā)人員,被指定的人員可以修改或者添加倉庫的接口嗜桌,未被指派的人員僅能查看接口奥溺,每個倉庫都擁有一個特定的倉庫域名前綴。其下的接口域名規(guī)則都遵循:${倉庫前綴域名}${接口配置域名}
骨宠,且每個倉庫都提供一個接口獲取當前倉庫數(shù)據(jù)浮定。
接口
我們先來看看接口配置頁面的組成:
可以看到接口頁面主要由如下部分組成:
- 新建接口(接口列表)
- 接口模塊
- 接口詳情(請求參數(shù)和響應(yīng)參數(shù))
在接口詳情中,請求的mock接口的路由是在新建接口的時候去創(chuàng)建的诱篷,創(chuàng)建之后自動生成一個接口壶唤,請求地址就是${倉庫域名}${接口路由}
雳灵。
請求參數(shù)的部分配置我們最主要要關(guān)注的是生成規(guī)則和默認值棕所,其規(guī)則和模板可以參考mockJs
的文檔中的語法規(guī)范,生成規(guī)則遵循數(shù)據(jù)模板定義規(guī)范(Data Template Definition悯辙,DTD)
琳省,默認值遵循數(shù)據(jù)占位符定義規(guī)范(Data Placeholder Definition,DPD)
躲撰。