分享內(nèi)容及技術(shù)棧本文將分享結(jié)合 京東智能設(shè)計(jì)平臺(tái)羚瓏 項(xiàng)目自身情況搭建的測(cè)試工作流的實(shí)踐經(jīng)驗(yàn),針對(duì)于 Node.js 服務(wù)端應(yīng)用的工具方法和接口的單元測(cè)試、集成測(cè)試等。實(shí)踐經(jīng)驗(yàn)?zāi)芙o你帶來(lái): 利用 Jest 搭建一套開(kāi)發(fā)體驗(yàn)友好的測(cè)試工作流。 書(shū)寫(xiě)一個(gè)高效的單元測(cè)試用例,及集成測(cè)試用例。 利用封裝技術(shù)實(shí)現(xiàn)模塊間的分離,簡(jiǎn)化測(cè)試代碼。 使用 SuperTest 完成應(yīng)用進(jìn)程與測(cè)試進(jìn)程的合并。 創(chuàng)建高效的數(shù)據(jù)庫(kù)內(nèi)存服務(wù),實(shí)現(xiàn)彼此隔離的測(cè)試套件運(yùn)行機(jī)制。 了解模擬(Mock)、快照(snapshot)與測(cè)試覆蓋率等功能的使用。 理解 TDD 與 BDD。 ...
文中涉及的基礎(chǔ)技術(shù)棧有(需要了解的知識(shí)): TypeScript: JavaScript 語(yǔ)言的超集,提供類型系統(tǒng)和新 ES 語(yǔ)法支持。 SuperTest: HTTP 代理及斷言工具。 MongoDB: NoSQL 分布式文件存儲(chǔ)數(shù)據(jù)庫(kù)。 Mongoose: MongoDB 對(duì)象關(guān)系映射操作庫(kù)(ORM)。 Koa: 基礎(chǔ) Web 應(yīng)用程序框架。 Jest: 功能豐富的 JavaScript 測(cè)試框架。 lodash: JavaScript 工具函數(shù)庫(kù)。
關(guān)于羚瓏
羚瓏 是京東旗下智能設(shè)計(jì)平臺(tái),提供在線設(shè)計(jì)服務(wù),主要包括大類如: 圖片設(shè)計(jì):快速合成廣告圖,主圖,公眾號(hào)配圖,海報(bào),傳單,物流面單等線上與線下設(shè)計(jì)服務(wù)。 視頻設(shè)計(jì):快速合成主圖視頻,抖音短視頻,自定義視頻等設(shè)計(jì)服務(wù)。 頁(yè)面設(shè)計(jì):快速搭建活動(dòng)頁(yè),營(yíng)銷(xiāo)頁(yè),小游戲,小程序等設(shè)計(jì)服務(wù)。 實(shí)用工具:批量摳圖、改尺寸、配色、加水印等。
基于行業(yè)領(lǐng)先技術(shù),為商家、用戶提供豐富的設(shè)計(jì)能力,實(shí)現(xiàn)快速產(chǎn)出。 羚瓏架構(gòu)及測(cè)試框架選型先介紹下羚瓏項(xiàng)目的架構(gòu),方便后續(xù)的描述和理解。羚瓏項(xiàng)目采用前后端分離的機(jī)制,前端采用 React Family 的基礎(chǔ)架構(gòu),再加上 Next.js 服務(wù)端渲染以提供更好的用戶體驗(yàn)及 SEO 排名。后端架構(gòu)則如下圖所示,流程大概是 瀏覽器或第三方應(yīng)用訪問(wèn)項(xiàng)目 Nginx 集群,Nginx 集群再通過(guò)負(fù)載均衡轉(zhuǎn)發(fā)到羚瓏應(yīng)用服務(wù)器,應(yīng)用服務(wù)器再通過(guò)對(duì)接外部服務(wù)或內(nèi)部服務(wù)等,或讀寫(xiě)緩存、數(shù)據(jù)庫(kù),邏輯處理后通過(guò) HTTP 返回到前端正確的數(shù)據(jù)。 
主流測(cè)試框架對(duì)比接下來(lái),根據(jù)項(xiàng)目所需我們對(duì)比下當(dāng)下 Node.js 端主流的測(cè)試框架。
| Jest | Mocha | AVA | Jasmine |
---|
GitHub Stars | 28.5K | 18.7K | 17.1K | 14.6K | GitHub Used by | 1.5M | 926K | 46.6K | 5.3K | 文檔友好 | 優(yōu)秀 | 良好 | 良好 | 良好 | 模擬功能(Mock) | 支持 | 外置 | 外置 | 外置 | 快照功能(Snapshot) | 支持 | 外置 | 支持 | 外置 | 支持 TypeScript | ts-jest | ts-mocha | ts-node | jasmine-ts | 詳細(xì)的錯(cuò)誤輸出 | 支持 | 支持 | 支持 | 未知 | 支持并行與串行 | 支持 | 外置 | 支持 | 外置 | 每個(gè)測(cè)試進(jìn)程隔離 | 支持 | 不支持 | 支持 | 未知 |
*文檔友好:文檔結(jié)構(gòu)組織有序,API 闡述完整,以及示例豐富。 分析: 之所以 Mocha GitHub 使用率很高,很有可能是因?yàn)槌霈F(xiàn)的最早(2011年),并由 Node.js 屆頂級(jí)開(kāi)發(fā)者 TJ 領(lǐng)導(dǎo)開(kāi)發(fā)的(后轉(zhuǎn)向Go語(yǔ)言),所以早期項(xiàng)目選擇了 Mocha 做為測(cè)試框架,而 Jest、AVA 則是后起之秀(2014年),并且 Stars 數(shù)量都在攀升,預(yù)計(jì)新項(xiàng)目都會(huì)在這兩個(gè)框架中挑選。 相比外置功能,內(nèi)置支持可能會(huì)與框架融合的更好,理念更趨近,維護(hù)更頻繁,使用更省心。 Jest 模擬功能可以實(shí)現(xiàn)方法模擬,定時(shí)器模擬,模塊/文件依賴模擬,在實(shí)際編寫(xiě)測(cè)試用例中,模擬模塊功能(mock modules)被常常用到,它可以確保測(cè)試用例快速響應(yīng)并且不會(huì)變化無(wú)常。下文也會(huì)談到如何使用它,為什么需要使用它。
綜上,我們選擇了 Jest 作為基礎(chǔ)測(cè)試框架。 從0到1落地實(shí)踐Jest 框架配置接下來(lái),我們從 0 到 1 開(kāi)始實(shí)踐,首先是搭建測(cè)試流,雖然 Jest 可以達(dá)到開(kāi)箱即用,然而項(xiàng)目架構(gòu)不盡相同,大多時(shí)候需要根據(jù)實(shí)際情況做些基礎(chǔ)配置工作。以下是根據(jù)羚瓏項(xiàng)目提取出來(lái)的簡(jiǎn)化版項(xiàng)目目錄結(jié)構(gòu),如下。 ├─ dist # TS 編譯結(jié)果目錄
├─ src # TS 源碼目錄
│ ├─ app.ts # 應(yīng)用主文件,類似 Express 框架的 /app.js 文件
│ └─ index.ts # 應(yīng)用啟動(dòng)文件,類似 Express 框架的 /bin/www 文件
├─ test # 測(cè)試文件目錄
│ ├─ @fixtures # 測(cè)試固定數(shù)據(jù)
│ ├─ @helpers # 測(cè)試工具方法
│ ├─ module1 # 模塊1的測(cè)試套件集合
│ │ └─ test-suite.ts # 測(cè)試套件,一類測(cè)試用例集合
│ └─ module2 # 模塊2的測(cè)試套件集合
├─ package.json
└─ yarn.lock
這里有兩個(gè)小點(diǎn): 以 @ 開(kāi)頭的目錄,我們定義為特殊文件目錄,用于提供些測(cè)試輔助工具方法、配置文件等,平級(jí)的其他目錄則是測(cè)試用例所在的目錄,按業(yè)務(wù)模塊或功能劃分。以 @ 開(kāi)頭可以清晰的顯示在同級(jí)目錄最上方,很容易開(kāi)發(fā)定位,湊巧也方便了編寫(xiě)正則匹配。 test-suite.ts 是項(xiàng)目?jī)?nèi)最小測(cè)試文件單元,我們稱之為測(cè)試套件,表示同一類測(cè)試用例的集合,可以是某個(gè)通用函數(shù)的多個(gè)測(cè)試用例集合,也可以是一個(gè)系列的單元測(cè)試用例集合。
首先安裝測(cè)試框架。 yarn add --dev jest ts-jest @types/jest
因?yàn)轫?xiàng)目是用 TypeScript 編寫(xiě),所以這里同時(shí)安裝 ts-jest @types/jest 。然后在根目錄新建 jest.config.js 配置文件,并做如下小許配置。 module.exports = {
// preset: 'ts-jest',
globals: {
'ts-jest': {
tsConfig: 'tsconfig.test.json',
},
},
testEnvironment: 'node',
roots: ['<rootDir>/src/', '<rootDir>/test/'],
testMatch: ['<rootDir>/test/**/*.ts'],
testPathIgnorePatterns: ['<rootDir>/test/@.+/'],
moduleNameMapper: {
'^~/(.*)': '<rootDir>/src/$1',
},
}
preset: 預(yù)設(shè)測(cè)試運(yùn)行環(huán)境,多數(shù)情況設(shè)置為 ts-jest 即可,如果需要為 ts-jest 指定些參數(shù),如上面指定 TS 配置為 tsconfig.test.json ,則需要像上面這樣的寫(xiě)法,將 ts-jest 掛載到 globals 屬性上,更多配置可以移步其官方文檔,這里。 testEnvironment: 基于預(yù)設(shè)再設(shè)置測(cè)試環(huán)境,Node.js 需要設(shè)置為 node ,因?yàn)槟J(rèn)值為瀏覽器環(huán)境 jsdom 。 roots: 用于設(shè)定測(cè)試監(jiān)聽(tīng)的目錄,如果匹配到的目錄的文件有所改動(dòng),就會(huì)自動(dòng)運(yùn)行測(cè)試用例。<rootDir> 表示項(xiàng)目根目錄,即與 package.json 同級(jí)的目錄。這里我們監(jiān)聽(tīng) src 和 test 兩個(gè)目錄。 testMatch: Glob 模式設(shè)置匹配的測(cè)試文件,當(dāng)然也可以是正則模式,這里我們匹配 test 目錄下的所有文件,匹配到的文件才會(huì)當(dāng)做測(cè)試用例執(zhí)行。 testPathIgnorePatterns: 設(shè)置已經(jīng)匹配到的但需要被忽略的文件,這里我們?cè)O(shè)置以 @ 開(kāi)頭的目錄及其所有文件都不當(dāng)做測(cè)試用例。 moduleNameMapper: 這個(gè)與 TS paths 和 Webpack alias 雷同,用于設(shè)置目錄別名,可以減少引用文件時(shí)的出錯(cuò)率并且提高開(kāi)發(fā)效率。這里我們?cè)O(shè)置以 ~ 開(kāi)頭的模塊名指向 src 目錄。 第一個(gè)單元測(cè)試用例搭建好測(cè)試運(yùn)行環(huán)境,于是便可著手編寫(xiě)測(cè)試用例了,下面我們編寫(xiě)一個(gè)接口單元測(cè)試用例,比方說(shuō)測(cè)試首頁(yè)輪播圖接口的正確性。我們將測(cè)試用例放在 test/homepage/carousel.ts 文件內(nèi),代碼如下。 import { forEach, isArray } from 'lodash’
import { JFSRegex, URLRegex } from '~/utils/regex'
import request from 'request-promise'
const baseUrl = 'http://ling-dev.jd.com/server/api'
// 聲明一個(gè)測(cè)試用例
test('輪播圖個(gè)數(shù)應(yīng)該返回 5,并且數(shù)據(jù)正確', async () => {
// 對(duì)接口發(fā)送 HTTP 請(qǐng)求
const res = await request.get(baseUrl + '/carousel/pictures')
// 校驗(yàn)返回狀態(tài)碼為 200
expect(res.statusCode).toBe(200)
// 校驗(yàn)返回?cái)?shù)據(jù)是數(shù)組并且長(zhǎng)度為 5
expect(isArray(res.body)).toBe(true)
expect(res.body.length).toBe(5)
// 校驗(yàn)數(shù)據(jù)每一項(xiàng)都是包含正確的 url, href 屬性的對(duì)象
forEach(res.body, picture => {
expect(picture).toMatchObject({
url: expect.stringMatching(JFSRegex),
href: expect.stringMatching(URLRegex),
})
})
})
編寫(xiě)好測(cè)試用例后,第一步需要啟動(dòng)應(yīng)用服務(wù)器: 
第二步運(yùn)行測(cè)試,在命令行窗口輸入:npx jest ,如下圖可以看到用例測(cè)試通過(guò)。 
當(dāng)然最佳實(shí)踐則是把命令封裝到 package.json 里,如下: {
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
}
}
之后便可使用 yarn test 來(lái)運(yùn)行測(cè)試,通過(guò) yarn test:watch 來(lái)啟動(dòng)監(jiān)聽(tīng)式測(cè)試服務(wù)。 SuperTest 增強(qiáng)雖然上面已經(jīng)完成基本的測(cè)試流程開(kāi)發(fā),但很明顯的一個(gè)問(wèn)題是每次運(yùn)行測(cè)試,我們需要先啟動(dòng)應(yīng)用服務(wù),共啟動(dòng)兩個(gè)進(jìn)程,并且需要提前配置 ling-dev.jd.com 指向 127.0.0.1:3800 ,這是一個(gè)繁瑣的過(guò)程。所以我們引入了 SuperTest ,它可以把應(yīng)用服務(wù)集成到測(cè)試服務(wù)一起啟動(dòng),并且不需要指定 HTTP 請(qǐng)求的主機(jī)地址。 我們封裝一個(gè)公共的 request 方法,將它放在 @helpers/agent.ts 文件內(nèi),如下。 import http from 'http'
import supertest from 'supertest'
import app from '~/app'
export const request = supertest(http.createServer(app.callback()))
解釋: 使用 app.callback() 而不是 app.listen() ,是因?yàn)樗梢詫⑼粋€(gè) app 同時(shí)作為 HTTP 和 HTTPS 或多個(gè)地址。app.callback() 返回適用于 http.createServer() 方法的回調(diào)函數(shù)來(lái)處理請(qǐng)求。 之后,http.createServer() 創(chuàng)建一個(gè)未監(jiān)聽(tīng)的 HTTP 對(duì)象給 SuperTest,當(dāng)然 SuperTest 內(nèi)部也會(huì)調(diào)用 listen(0) 這樣的特殊端口,讓操作系統(tǒng)提供可用的隨機(jī)端口來(lái)啟動(dòng)應(yīng)用服務(wù)器。
所以上面的測(cè)試用例我們可以改寫(xiě)成這樣: import { forEach, isArray } from 'lodash’
import { JFSRegex, URLRegex } from '~/utils/regex'
// 引入公共的 request 方法
import { request } from '../@helpers/agent'
test('輪播圖個(gè)數(shù)應(yīng)該返回 5,并且數(shù)據(jù)正確', async () => {
const res = await request.get('/api/carousel/pictures')
expect(res.status).toBe(200)
// 同樣的校驗(yàn)...
})
因?yàn)?SuperTest 內(nèi)部已經(jīng)幫我們包裝好了主機(jī)地址并自動(dòng)啟動(dòng)應(yīng)用服務(wù),所以請(qǐng)求接口時(shí)只需書(shū)寫(xiě)具體的接口,如 /api/carousel/pictures ,也只需運(yùn)行一條命令 yarn test ,就可以完成整個(gè)測(cè)試工作。 數(shù)據(jù)庫(kù)內(nèi)存服務(wù)項(xiàng)目架構(gòu)中可以看到數(shù)據(jù)庫(kù)使用的是 MongoDB,在測(cè)試時(shí),幾乎所有的接口都需要與數(shù)據(jù)庫(kù)連接。此時(shí)可通過(guò)環(huán)境變量區(qū)分并新建 test 數(shù)據(jù)庫(kù),用于運(yùn)行測(cè)試用例。有點(diǎn)不好的是測(cè)試套件執(zhí)行完成后需要對(duì) test 數(shù)據(jù)庫(kù)進(jìn)行清空,以避免臟數(shù)據(jù)影響下個(gè)測(cè)試套件,尤其是在并發(fā)運(yùn)行時(shí),需要保持?jǐn)?shù)據(jù)隔離。 使用 MongoDB Memory Server 是更好的選擇,它會(huì)啟動(dòng)獨(dú)立的 MongoDB 實(shí)例(每個(gè)實(shí)例大約占用非常低的 7MB 內(nèi)存),而測(cè)試套件將運(yùn)行在這個(gè)獨(dú)立的實(shí)例里。假如并發(fā)為 3,那就創(chuàng)建 3 個(gè)實(shí)例分別運(yùn)行 3 個(gè)測(cè)試套件,這樣可以很好的保持?jǐn)?shù)據(jù)隔離,并且數(shù)據(jù)都保存在內(nèi)存中,這使得運(yùn)行速度會(huì)非常快,當(dāng)測(cè)試套件完成后則自動(dòng)銷(xiāo)毀實(shí)例。 
接下來(lái)我們把 MongoDB Memory Server 引入實(shí)際測(cè)試中,最佳方式是把它寫(xiě)進(jìn) Jest 環(huán)境配置里,這樣只需要一次書(shū)寫(xiě),自動(dòng)運(yùn)行在每個(gè)測(cè)試套件中。所以替換 jest.config.js 配置文件的 testEnvironment 為自定義環(huán)境 <rootDir>/test/@helpers/jest-env.js 。 編寫(xiě)自定義環(huán)境 @helpers/jest-env.js : const NodeEnvironment = require('jest-environment-node')
const { MongoMemoryServer } = require('mongodb-memory-server')
const child_process = require('child_process')
// 繼承 Node 環(huán)境
class CustomEnvironment extends NodeEnvironment {
// 在測(cè)試套件啟動(dòng)前,獲取本地開(kāi)發(fā) MongoDB Uri 并注入 global 對(duì)象
async setup() {
const uri = await getMongoUri()
this.global.testConfig = {
mongo: { uri },
}
await super.setup()
}
}
async function getMongoUri() {
// 通過(guò) which mongod 命令拿到本地 MongoDB 二進(jìn)制文件路徑
const mongodPath = await new Promise((resolve, reject) => {
child_process.exec(
'which mongod',
{ encoding: 'utf8' },
(err, stdout, stderr) => {
if (err || stderr) {
return reject(
new Error('找不到系統(tǒng)的 mongod,請(qǐng)確保 `which mongod` 可以指向 mongod')
)
}
resolve(stdout.trim())
}
)
})
// 使用本地 MongoDB 二進(jìn)制文件創(chuàng)建內(nèi)存服務(wù)實(shí)例
const mongod = new MongoMemoryServer({
binary: { systemBinary: mongodPath },
})
// 得到創(chuàng)建成功的實(shí)例 Uri 地址
const uri = await mongod.getConnectionString()
return uri
}
// 導(dǎo)出自定義環(huán)境類
module.exports = CustomEnvironment
Mongoose 中便可以這樣連接: await mongoose.connect((global as any).testConfig.mongo.uri, {
useNewUrlParser: true,
useUnifiedTopology: true,
})
當(dāng)然在 package.json 里需要禁用 MongoDB Memory Server 去下載二進(jìn)制包,因?yàn)樯厦嬉呀?jīng)使用了本地二進(jìn)制包。 "config": {
"mongodbMemoryServer": {
"version": "4.0",
// 禁止在 yarn install 時(shí)下載二進(jìn)制包
"disablePostinstall": "1",
"md5Check": "1"
}
}
登錄功能封裝與使用大多時(shí)候接口是需要登錄后才能訪問(wèn)的,所以我們需要把整塊登錄功能抽離出來(lái),封裝成通用方法,同時(shí)借此初始化一些測(cè)試專用數(shù)據(jù)。 為了使 API 易用,我希望登錄 API 長(zhǎng)這樣: import { login } from '../@helpers/login'
// 調(diào)用登錄方法,根據(jù)傳遞的角色創(chuàng)建用戶,并返回該用戶登錄的 request 對(duì)象。
// 支持多參數(shù),根據(jù)參數(shù)不同自動(dòng)初始化測(cè)試數(shù)據(jù)。
const request = await login({
role: 'user',
})
// 使用已登錄的 request 對(duì)象訪問(wèn)需要登錄的用戶接口,
// 應(yīng)當(dāng)是登錄態(tài),并正確返回當(dāng)前登錄的用戶信息。
const res = await request.get('/api/user/info')
開(kāi)發(fā)登錄方法: // @helpers/agent.ts
// 新添加 makeAgent 方法
export function makeAgent() {
// 使用 supertest.agent 支持 cookie 持久化
return supertest.agent(http.createServer(app.callback()))
}
// @helpers/login.ts
import { assign, cloneDeep, pick } from 'lodash'
import { makeAgent } from './agent'
export async function login(userData: UserDataType): Promise<RequestAgent> {
userData = cloneDeep(userData)
// 如果沒(méi)有用戶名,自動(dòng)創(chuàng)建用戶名
if (!userData.username) {
userData.username = chance.word({ length: 8 })
}
// 如果沒(méi)有昵稱,自動(dòng)創(chuàng)建昵稱
if (!userData.nickname) {
userData.nickname = chance.word({ length: 8 })
}
// 得到支持 cookie 持久化的 request 對(duì)象
const request: any = makeAgent()
// 發(fā)送登錄請(qǐng)求,這里為測(cè)試專門(mén)設(shè)計(jì)一個(gè)登錄接口
// 包含正常登錄功能,但還會(huì)根據(jù)傳參不同初始化測(cè)試專用數(shù)據(jù)
const res = await request.post('/api/login-test').send(userData)
// 將登錄返回的數(shù)據(jù)賦值到 request 對(duì)象上
assign(request, pick(res.body, ['user', 'otherValidKey...']))
// 返回 request 對(duì)象
return request as RequestAgent
}
實(shí)際用例中就像上面示例方式使用。 模擬功能使用從項(xiàng)目架構(gòu)中可以看到項(xiàng)目也會(huì)調(diào)用較多外部服務(wù)。比方說(shuō)創(chuàng)建文件夾的接口,內(nèi)部代碼需要調(diào)用外部服務(wù)去鑒定文件夾名稱是否包含敏感詞,就像這樣: import { detectText } from '~/utils/detect'
// 調(diào)用外部服務(wù)檢測(cè)文件夾名稱是否包含敏感詞
const { ok, sensitiveWords } = await detectText(folderName)
if (!ok) {
throw new Error(`檢測(cè)到敏感詞: ${sensitiveWords}`)
}
實(shí)際測(cè)試的時(shí)候并不需要所有測(cè)試用例運(yùn)行時(shí)都調(diào)用外部服務(wù),這樣會(huì)拖慢測(cè)試用例的響應(yīng)時(shí)間以及不穩(wěn)定性。我們可以建立個(gè)更好的機(jī)制,新建一個(gè)測(cè)試套件專門(mén)用于驗(yàn)證 detectText 工具方法的正確性,而其他測(cè)試套件運(yùn)行時(shí) detectText 方法直接返回 OK 即可,這樣既保證了 detectText 方法被驗(yàn)證到,也保證了其他測(cè)試套件得到快速響應(yīng)。 模擬功能(Mock)就是為這樣的情景而誕生的。我們只需要在 detectText 方法的路徑 utils/detect.ts 同級(jí)新建__mocks__/detect.ts 模擬文件即可,內(nèi)容如下,直接返回結(jié)果: export async function detectText(
text: string
): Promise<{ ok: boolean; sensitive: boolean; sensitiveWords?: string }> {
// 刪除所有代碼,直接返回 OK
return { ok: true, sensitive: false }
}
之后每個(gè)需要模擬的測(cè)試套件頂部加上下面一句代碼即可。 jest.mock('~/utils/detect.ts')
在驗(yàn)證 detectText 工具方法的測(cè)試套件里,則只需 jest.unmock 即可恢復(fù)真實(shí)的方法。 jest.unmock('~/utils/detect.ts')
當(dāng)然應(yīng)該把 jest.mock 寫(xiě)在 setupFiles 配置里,因?yàn)樾枰M的測(cè)試套件占絕大多數(shù),寫(xiě)在配置里會(huì)讓它們?cè)谶\(yùn)行前自動(dòng)加載該文件,這樣開(kāi)發(fā)就不必每處測(cè)試套件都加上一段同樣的代碼,可以有效提高開(kāi)發(fā)效率。 // jest.config.js
setupFiles: ['<rootDir>/test/@helpers/jest-setup.ts']
// @helpers/jest-setup.ts
jest.mock('~/utils/detect.ts')
模擬功能還有方法模擬,定時(shí)器模擬等,可以查閱其文檔了解更多示例。 快照功能使用快照功能(Snapshot) 可以幫我們測(cè)試大型對(duì)象,從而簡(jiǎn)化測(cè)試用例。
舉個(gè)例子,項(xiàng)目的模板解析接口,該接口會(huì)將 PSD 模板文件進(jìn)行解析,然后吐出一個(gè)較大的 JSON 數(shù)據(jù),如果挨個(gè)校驗(yàn)對(duì)象的屬性是否正確可能很不理想,所以可以使用快照功能,就是第一次運(yùn)行測(cè)試用例時(shí),會(huì)把 JSON 數(shù)據(jù)存儲(chǔ)到本地文件,稱之為快照文件,第二次運(yùn)行時(shí),就會(huì)將第二次返回的數(shù)據(jù)與快照文件進(jìn)行比較,如果兩個(gè)快照匹配,則表示測(cè)試成功,反之測(cè)試失敗。 而使用方式很簡(jiǎn)單: // 請(qǐng)求模板解析接口
const res = await request.post('/api/secret/parser')
// 斷言快照是否匹配
expect(res.body).toMatchSnapshot()
更新快照也是敏捷的,運(yùn)行命令 jest --updateSnapshot 或在監(jiān)聽(tīng)模式輸入 u 來(lái)更新。 集成測(cè)試集成測(cè)試的概念是在單元測(cè)試的基礎(chǔ)上,將所有模塊按照一定要求或流程關(guān)系進(jìn)行串聯(lián)測(cè)試。比方說(shuō),一些模塊雖然能夠單獨(dú)工作,但并不能保證連接起來(lái)也能正常工作,一些局部反映不出來(lái)的問(wèn)題,在全局上很可能暴露出來(lái)。 因?yàn)闇y(cè)試框架 Jest 對(duì)于每個(gè)測(cè)試套件是并行運(yùn)行的,而套件內(nèi)的用例則是串行運(yùn)行的,所以編寫(xiě)集成測(cè)試很方便,下面我們用文件夾的使用流程示例如何完成集成測(cè)試的編寫(xiě)。 import { request } from '../@helpers/agent'
import { login } from '../@helpers/login'
const urlCreateFolder = '/api/secret/folder' // POST
const urlFolderDetails = '/api/secret/folder' // GET
const urlFetchFolders = '/api/secret/folders' // GET
const urlDeleteFolder = '/api/secret/folder' // DELETE
const urlRenameFolder = '/api/secret/folder/rename' // PUT
const folders: ObjectAny[] = []
let globalReq: ObjectAny
test('沒(méi)有權(quán)限創(chuàng)建文件夾應(yīng)該返回 403 錯(cuò)誤', async () => {
const res = await request.post(urlCreateFolder).send({
name: '我的文件夾',
})
expect(res.status).toBe(403)
})
test('確保創(chuàng)建 3 個(gè)文件夾', async () => {
// 登錄有權(quán)限創(chuàng)建文件夾的用戶,比如設(shè)計(jì)師
globalReq = await login({ role: 'designer' })
for (let i = 0; i < 3; i++) {
const res = await globalReq.post(urlCreateFolder).send({
name: '我的文件夾' + i,
})
// 將創(chuàng)建成功的文件夾置入 folders 常量里
folders.push(res.body)
expect(res.status).toBe(200)
// 更多驗(yàn)證規(guī)則...
}
})
test('重命名第 2 個(gè)文件夾', async () => {
const res = await globalReq.put(urlRenameFolder).send({
id: folders[1].id,
name: '新文件夾名稱',
})
expect(res.status).toBe(200)
})
test('第 2 個(gè)文件夾的名稱應(yīng)該是【新文件夾名稱】', async () => {
const res = await globalReq.get(urlFolderDetails).query({
id: folders[1].id,
})
expect(res.status).toBe(200)
expect(res.body.name).toBe('新文件夾名稱')
// 更多驗(yàn)證規(guī)則...
})
test('獲取文件夾列表應(yīng)該返回 3 條數(shù)據(jù)', async () => {
// 與上雷同,鑒于代碼過(guò)多,先行省略...
})
test('刪除最后一個(gè)文件夾', async () => {
// 與上雷同,鑒于代碼過(guò)多,先行省略...
})
test('再次獲取文件夾列表應(yīng)該返回 2 條數(shù)據(jù)', async () => {
// 與上雷同,鑒于代碼過(guò)多,先行省略...
})
測(cè)試覆蓋率測(cè)試覆蓋率是對(duì)測(cè)試完成程度的評(píng)測(cè),基于文件被測(cè)試的情況來(lái)反饋測(cè)試的質(zhì)量。 運(yùn)行命令 jest --coverage 即可生成測(cè)試覆蓋率報(bào)告,打開(kāi)生成的 coverage/lcov-report/index.html 文件,各項(xiàng)指標(biāo)一覽無(wú)余。因?yàn)?Jest 內(nèi)部使用 Istanbul 生成覆蓋率報(bào)告,所以各項(xiàng)指標(biāo)依然參考 Istanbul。 
持續(xù)集成寫(xiě)完這么多測(cè)試用例之后,或者是開(kāi)發(fā)完功能代碼后,我們是不是希望每次將代碼推送到托管平臺(tái),如 GitLab,托管平臺(tái)能自動(dòng)幫我們運(yùn)行所有測(cè)試用例,如果測(cè)試失敗就郵件通知我們修復(fù),如果測(cè)試通過(guò)則把開(kāi)發(fā)分支合并到主分支? 答案是必須的。這就與持續(xù)集成(Continuous Integration)不謀而合,通俗的講就是經(jīng)常性地將代碼合并到主干分支,每次合并前都需要運(yùn)行自動(dòng)化測(cè)試以驗(yàn)證代碼的正確性。 所以我們配置一些自動(dòng)化測(cè)試任務(wù),按順序執(zhí)行安裝、編譯、測(cè)試等命令,測(cè)試命令則是運(yùn)行編寫(xiě)好的測(cè)試用例。一個(gè) GitLab 的配置任務(wù)(.gitlab-ci.yml )可能像下面這樣,僅作參考。 # 每個(gè) job 之前執(zhí)行的命令
before_script:
- echo "`whoami` ($0 $SHELL)"
- echo "`which node` (`node -v`)"
- echo $CI_PROJECT_DIR
# 定義 job 所屬 test 階段及執(zhí)行的命令等
test:
stage: test
except:
- test
cache:
paths:
- node_modules/
script:
- yarn
- yarn lint
- yarn test
# 定義 job 所屬 deploy 階段及執(zhí)行的命令等
deploy-stage:
stage: deploy
only:
- test
script:
- cd /app
- make BRANCH=origin/${CI_COMMIT_REF_NAME} deploy-stage
持續(xù)集成的好處: 快速發(fā)現(xiàn)錯(cuò)誤。 防止分支大幅偏離主干分支。 讓產(chǎn)品可以快速迭代,同時(shí)還能保持高質(zhì)量。
TDD與BDD引入TDD 全稱測(cè)試驅(qū)動(dòng)開(kāi)發(fā)(Test-driven development),是敏捷開(kāi)發(fā)中的一種設(shè)計(jì)方法論,強(qiáng)調(diào)先將需求轉(zhuǎn)換為具體的測(cè)試用例,然后再開(kāi)發(fā)代碼以使測(cè)試通過(guò)。 BDD 全稱行為驅(qū)動(dòng)開(kāi)發(fā)(Behavior-driven development),也是一種敏捷開(kāi)發(fā)設(shè)計(jì)方法論,它沒(méi)有強(qiáng)調(diào)具體的形式如何,而是強(qiáng)調(diào)【作為什么角色,想要什么功能,以便收益什么】這樣的用戶故事指定行為的論點(diǎn)。 兩者都是很好的開(kāi)發(fā)模式,結(jié)合實(shí)際情況,我們的測(cè)試更像是 BDD,不過(guò)并沒(méi)有完全摒棄 TDD,我們的建議是如果覺(jué)得先寫(xiě)測(cè)試可以幫助更快的寫(xiě)好代碼,那就先寫(xiě)測(cè)試,如果覺(jué)得先寫(xiě)代碼再寫(xiě)測(cè)試,或一邊開(kāi)發(fā)一邊測(cè)試更好,則采用自己的方式,而結(jié)果是編碼功能和測(cè)試用例都需要完成,并且運(yùn)行通過(guò),最后通過(guò) Code Review 對(duì)代碼質(zhì)量做進(jìn)一步審查與把控。 筆者稱之為【師夷長(zhǎng)技,聚于自身】:結(jié)合項(xiàng)目自身的實(shí)際情況,靈活變通,形成一套適合自身項(xiàng)目發(fā)展的模式驅(qū)動(dòng)開(kāi)發(fā)。 結(jié)論自動(dòng)化測(cè)試提供了一種有保障的機(jī)制檢測(cè)整個(gè)系統(tǒng),可以頻繁地進(jìn)行回歸測(cè)試,有效提高系統(tǒng)穩(wěn)定性。當(dāng)然編寫(xiě)與維護(hù)測(cè)試用例需要耗費(fèi)一定的成本,需要考慮投入與產(chǎn)出效益之間的平衡。 歡迎關(guān)注凹凸實(shí)驗(yàn)室博客:aotu.io 或者關(guān)注凹凸實(shí)驗(yàn)室公眾號(hào)(AOTULabs),不定時(shí)推送文章: 
|