日韩黑丝制服一区视频播放|日韩欧美人妻丝袜视频在线观看|九九影院一级蜜桃|亚洲中文在线导航|青草草视频在线观看|婷婷五月色伊人网站|日本一区二区在线|国产AV一二三四区毛片|正在播放久草视频|亚洲色图精品一区

分享

羚瓏項(xiàng)目自動(dòng)化測(cè)試方案實(shí)踐

 小世界的野孩子 2020-02-13

分享內(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):

  1. 利用 Jest 搭建一套開(kāi)發(fā)體驗(yàn)友好的測(cè)試工作流。

  2. 書(shū)寫(xiě)一個(gè)高效的單元測(cè)試用例,及集成測(cè)試用例。

  3. 利用封裝技術(shù)實(shí)現(xiàn)模塊間的分離,簡(jiǎn)化測(cè)試代碼。

  4. 使用 SuperTest 完成應(yīng)用進(jìn)程與測(cè)試進(jìn)程的合并。

  5. 創(chuàng)建高效的數(shù)據(jù)庫(kù)內(nèi)存服務(wù),實(shí)現(xiàn)彼此隔離的測(cè)試套件運(yùn)行機(jī)制。

  6. 了解模擬(Mock)、快照(snapshot)與測(cè)試覆蓋率等功能的使用。

  7. 理解 TDD 與 BDD。

  8. ...

文中涉及的基礎(chǔ)技術(shù)棧有(需要了解的知識(shí)):

  1. TypeScript: JavaScript 語(yǔ)言的超集,提供類型系統(tǒng)和新 ES 語(yǔ)法支持。

  2. SuperTest: HTTP 代理及斷言工具。

  3. MongoDB: NoSQL 分布式文件存儲(chǔ)數(shù)據(jù)庫(kù)。

  4. Mongoose: MongoDB 對(duì)象關(guān)系映射操作庫(kù)(ORM)。

  5. Koa: 基礎(chǔ) Web 應(yīng)用程序框架。

  6. Jest: 功能豐富的 JavaScript 測(cè)試框架。

  7. lodash: JavaScript 工具函數(shù)庫(kù)。

關(guān)于羚瓏

羚瓏Logo

羚瓏 是京東旗下智能設(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ù)。

羚瓏服務(wù)端架構(gòu)圖

主流測(cè)試框架對(duì)比

接下來(lái),根據(jù)項(xiàng)目所需我們對(duì)比下當(dāng)下 Node.js 端主流的測(cè)試框架。


JestMochaAVAJasmine
GitHub Stars28.5K18.7K17.1K14.6K
GitHub Used by1.5M926K46.6K5.3K
文檔友好優(yōu)秀良好良好良好
模擬功能(Mock)支持外置外置外置
快照功能(Snapshot)支持外置支持外置
支持 TypeScriptts-jestts-mochats-nodejasmine-ts
詳細(xì)的錯(cuò)誤輸出支持支持支持未知
支持并行與串行支持外置支持外置
每個(gè)測(cè)試進(jìn)程隔離支持不支持支持未知

*文檔友好:文檔結(jié)構(gòu)組織有序,API 闡述完整,以及示例豐富。

分析:

  1. 之所以 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è)框架中挑選。

  2. 相比外置功能,內(nèi)置支持可能會(huì)與框架融合的更好,理念更趨近,維護(hù)更頻繁,使用更省心。

  3. 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):

  1. @ 開(kāi)頭的目錄,我們定義為特殊文件目錄,用于提供些測(cè)試輔助工具方法、配置文件等,平級(jí)的其他目錄則是測(cè)試用例所在的目錄,按業(yè)務(wù)模塊或功能劃分。以 @ 開(kāi)頭可以清晰的顯示在同級(jí)目錄最上方,很容易開(kāi)發(fā)定位,湊巧也方便了編寫(xiě)正則匹配。

  2. 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) srctest 兩個(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 pathsWebpack 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īng)用服務(wù)運(yùn)行截圖

第二步運(yùn)行測(cè)試,在命令行窗口輸入:npx jest,如下圖可以看到用例測(cè)試通過(guò)。

測(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()))

解釋:

  1. 使用 app.callback() 而不是 app.listen(),是因?yàn)樗梢詫⑼粋€(gè) app 同時(shí)作為 HTTP 和 HTTPS 或多個(gè)地址。app.callback() 返回適用于 http.createServer() 方法的回調(diào)函數(shù)來(lái)處理請(qǐng)求。

  2. 之后,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í)例。

MongoDB Memory Server

接下來(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。

測(cè)試覆蓋率報(bào)告

持續(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ù)集成的好處:

  1. 快速發(fā)現(xiàn)錯(cuò)誤。

  2. 防止分支大幅偏離主干分支。

  3. 讓產(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í)推送文章:

13-橫_1575377567139.jpg

    本站是提供個(gè)人知識(shí)管理的網(wǎng)絡(luò)存儲(chǔ)空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請(qǐng)注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購(gòu)買(mǎi)等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請(qǐng)點(diǎn)擊一鍵舉報(bào)。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評(píng)論

    發(fā)表

    請(qǐng)遵守用戶 評(píng)論公約

    類似文章 更多