羚瓏項目自動化測試方案實踐

分享內容及技術棧

本文將分享結合 京東智能設計平臺羚瓏 項目自身狀況搭建的測試工做流的實踐經驗,針對於 Node.js 服務端應用的工具方法和接口的單元測試、集成測試等。實踐經驗能給你帶來:javascript

  1. 利用 Jest 搭建一套開發體驗友好的測試工做流。
  2. 書寫一個高效的單元測試用例,及集成測試用例。
  3. 利用封裝技術實現模塊間的分離,簡化測試代碼。
  4. 使用 SuperTest 完成應用進程與測試進程的合併。
  5. 建立高效的數據庫內存服務,實現彼此隔離的測試套件運行機制。
  6. 瞭解模擬(Mock)、快照(snapshot)與測試覆蓋率等功能的使用。
  7. 理解 TDD 與 BDD。
  8. ...

文中涉及的基礎技術棧有(須要瞭解的知識):html

  1. TypeScript: JavaScript 語言的超集,提供類型系統和新 ES 語法支持。
  2. SuperTest: HTTP 代理及斷言工具。
  3. MongoDB: NoSQL 分佈式文件存儲數據庫。
  4. Mongoose: MongoDB 對象關係映射操做庫(ORM)。
  5. Koa: 基礎 Web 應用程序框架。
  6. Jest: 功能豐富的 JavaScript 測試框架。
  7. lodash: JavaScript 工具函數庫。

關於羚瓏

羚瓏Logo

羚瓏 是京東旗下智能設計平臺,提供在線設計服務,主要包括大類如:前端

  • 圖片設計:快速合成廣告圖,主圖,公衆號配圖,海報,傳單,物流面單等線上與線下設計服務。
  • 視頻設計:快速合成主圖視頻,抖音短視頻,自定義視頻等設計服務。
  • 頁面設計:快速搭建活動頁,營銷頁,小遊戲,小程序等設計服務。
  • 實用工具:批量摳圖、改尺寸、配色、加水印等。

基於行業領先技術,爲商家、用戶提供豐富的設計能力,實現快速產出。java

羚瓏架構及測試框架選型

先介紹下羚瓏項目的架構,方便後續的描述和理解。羚瓏項目採用先後端分離的機制,前端採用 React Family 的基礎架構,再加上 Next.js 服務端渲染以提供更好的用戶體驗及 SEO 排名。後端架構則以下圖所示,流程大概是 瀏覽器或第三方應用訪問項目 Nginx 集羣,Nginx 集羣再經過負載均衡轉發到羚瓏應用服務器,應用服務器再經過對接外部服務或內部服務等,或讀寫緩存、數據庫,邏輯處理後經過 HTTP 返回到前端正確的數據。node

羚瓏服務端架構圖

主流測試框架對比

接下來,根據項目所需咱們對比下當下 Node.js 端主流的測試框架。git

Jest Mocha AVA Jasmine
GitHub Stars 28.5K 18.7K 17.1K 14.6K
GitHub Used by 1.5M 926K 46.6K 5.3K
文檔友好 優秀 良好 良好 良好
模擬功能(Mock) 支持 外置 外置 外置
快照功能(Snapshot) 支持 外置 支持 外置
支持 TypeScript ts-jest ts-mocha ts-node jasmine-ts
詳細的錯誤輸出 支持 支持 支持 未知
支持並行與串行 支持 外置 支持 外置
每一個測試進程隔離 支持 不支持 支持 未知

*文檔友好:文檔結構組織有序,API 闡述完整,以及示例豐富。github

分析:mongodb

  1. 之因此 Mocha GitHub 使用率很高,頗有多是由於出現的最先(2011年),並由 Node.js 屆頂級開發者 TJ 領導開發的(後轉向Go語言),因此早期項目選擇了 Mocha 作爲測試框架,而 JestAVA 則是後起之秀(2014年),而且 Stars 數量都在攀升,預計新項目都會在這兩個框架中挑選。
  2. 相比外置功能,內置支持可能會與框架融合的更好,理念更趨近,維護更頻繁,使用更省心。
  3. Jest 模擬功能能夠實現方法模擬,定時器模擬,模塊/文件依賴模擬,在實際編寫測試用例中,模擬模塊功能(mock modules)被經常用到,它能夠確保測試用例快速響應而且不會變化無常。下文也會談到如何使用它,爲何須要使用它。

綜上,咱們選擇了 Jest 做爲基礎測試框架。數據庫

從0到1落地實踐

Jest 框架配置

接下來,咱們從 0 到 1 開始實踐,首先是搭建測試流,雖然 Jest 能夠達到開箱即用,然而項目架構不盡相同,大多時候須要根據實際狀況作些基礎配置工做。如下是根據羚瓏項目提取出來的簡化版項目目錄結構,以下。json

├─ dist                 # TS 編譯結果目錄
├─ src                  # TS 源碼目錄
│   ├─ app.ts              # 應用主文件,相似 Express 框架的 /app.js 文件
│   └─ index.ts            # 應用啓動文件,相似 Express 框架的 /bin/www 文件
├─ test                 # 測試文件目錄
│   ├─ @fixtures           # 測試固定數據
│   ├─ @helpers            # 測試工具方法
│   ├─ module1             # 模塊1的測試套件集合
│   │  └─ test-suite.ts       # 測試套件,一類測試用例集合
│   └─ module2             # 模塊2的測試套件集合
├─ package.json           
└─ yarn.lock

這裏有兩個小點:

  1. @ 開頭的目錄,咱們定義爲特殊文件目錄,用於提供些測試輔助工具方法、配置文件等,平級的其餘目錄則是測試用例所在的目錄,按業務模塊或功能劃分。以 @ 開頭能夠清晰的顯示在同級目錄最上方,很容易開發定位,湊巧也方便了編寫正則匹配。
  2. test-suite.ts 是項目內最小測試文件單元,咱們稱之爲測試套件,表示同一類測試用例的集合,能夠是某個通用函數的多個測試用例集合,也能夠是一個系列的單元測試用例集合。

首先安裝測試框架。

yarn add --dev jest ts-jest @types/jest

由於項目是用 TypeScript 編寫,因此這裏同時安裝 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: 預設測試運行環境,多數狀況設置爲 ts-jest 便可,若是須要爲 ts-jest 指定些參數,如上面指定 TS 配置爲 tsconfig.test.json,則須要像上面這樣的寫法,將 ts-jest 掛載到 globals 屬性上,更多配置能夠移步其官方文檔,這裏

testEnvironment: 基於預設再設置測試環境,Node.js 須要設置爲 node,由於默認值爲瀏覽器環境 jsdom

roots: 用於設定測試監聽的目錄,若是匹配到的目錄的文件有所改動,就會自動運行測試用例。<rootDir> 表示項目根目錄,即與 package.json 同級的目錄。這裏咱們監聽 srctest 兩個目錄。

testMatch: Glob 模式設置匹配的測試文件,固然也能夠是正則模式,這裏咱們匹配 test 目錄下的全部文件,匹配到的文件纔會當作測試用例執行。

testPathIgnorePatterns: 設置已經匹配到的但須要被忽略的文件,這裏咱們設置以 @ 開頭的目錄及其全部文件都不當作測試用例。

moduleNameMapper: 這個與 TS pathsWebpack alias 雷同,用於設置目錄別名,能夠減小引用文件時的出錯率而且提升開發效率。這裏咱們設置以 ~ 開頭的模塊名指向 src 目錄。

第一個單元測試用例

搭建好測試運行環境,因而即可着手編寫測試用例了,下面咱們編寫一個接口單元測試用例,比方說測試首頁輪播圖接口的正確性。咱們將測試用例放在 test/homepage/carousel.ts 文件內,代碼以下。

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'

// 聲明一個測試用例
test('輪播圖個數應該返回 5,而且數據正確', async () => {
  // 對接口發送 HTTP 請求
  const res = await request.get(baseUrl + '/carousel/pictures')
  
  // 校驗返回狀態碼爲 200
  expect(res.statusCode).toBe(200)
  
  // 校驗返回數據是數組而且長度爲 5
  expect(isArray(res.body)).toBe(true)
  expect(res.body.length).toBe(5)
  
  // 校驗數據每一項都是包含正確的 url, href 屬性的對象
  forEach(res.body, picture => {
    expect(picture).toMatchObject({
      url: expect.stringMatching(JFSRegex),
      href: expect.stringMatching(URLRegex),
    })
  })
})

編寫好測試用例後,第一步須要啓動應用服務器:

應用服務運行截圖

第二步運行測試,在命令行窗口輸入:npx jest,以下圖能夠看到用例測試經過。

測試用例經過截圖

固然最佳實踐則是把命令封裝到 package.json 裏,以下:

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
  }
}

以後即可使用 yarn test 來運行測試,經過 yarn test:watch 來啓動監聽式測試服務。

SuperTest 加強

雖然上面已經完成基本的測試流程開發,但很明顯的一個問題是每次運行測試,咱們須要先啓動應用服務,共啓動兩個進程,而且須要提早配置 ling-dev.jd.com 指向 127.0.0.1:3800,這是一個繁瑣的過程。因此咱們引入了 SuperTest,它能夠把應用服務集成到測試服務一塊兒啓動,而且不須要指定 HTTP 請求的主機地址。

咱們封裝一個公共的 request 方法,將它放在 @helpers/agent.ts 文件內,以下。

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(),是由於它能夠將同一個 app 同時做爲 HTTP 和 HTTPS 或多個地址。app.callback() 返回適用於 http.createServer() 方法的回調函數來處理請求。
  2. 以後,http.createServer() 建立一個未監聽的 HTTP 對象給 SuperTest,固然 SuperTest 內部也會調用 listen(0) 這樣的特殊端口,讓操做系統提供可用的隨機端口來啓動應用服務器。

因此上面的測試用例咱們能夠改寫成這樣:

import { forEach, isArray } from 'lodash’
import { JFSRegex, URLRegex } from '~/utils/regex'

// 引入公共的 request 方法
import { request } from '../@helpers/agent'

test('輪播圖個數應該返回 5,而且數據正確', async () => {
  const res = await request.get('/api/carousel/pictures')
  expect(res.status).toBe(200)
  // 一樣的校驗...
})

由於 SuperTest 內部已經幫咱們包裝好了主機地址並自動啓動應用服務,因此請求接口時只需書寫具體的接口,如 /api/carousel/pictures,也只需運行一條命令 yarn test,就能夠完成整個測試工做。

數據庫內存服務

項目架構中能夠看到數據庫使用的是 MongoDB,在測試時,幾乎全部的接口都須要與數據庫鏈接。此時可經過環境變量區分並新建 test 數據庫,用於運行測試用例。有點很差的是測試套件執行完成後須要對 test 數據庫進行清空,以免髒數據影響下個測試套件,尤爲是在併發運行時,須要保持數據隔離。

使用 MongoDB Memory Server 是更好的選擇,它會啓動獨立的 MongoDB 實例(每一個實例大約佔用很是低的 7MB 內存),而測試套件將運行在這個獨立的實例裏。假如併發爲 3,那就建立 3 個實例分別運行 3 個測試套件,這樣能夠很好的保持數據隔離,而且數據都保存在內存中,這使得運行速度會很是快,當測試套件完成後則自動銷燬實例。

MongoDB Memory Server

接下來咱們把 MongoDB Memory Server 引入實際測試中,最佳方式是把它寫進 Jest 環境配置裏,這樣只須要一次書寫,自動運行在每一個測試套件中。因此替換 jest.config.js 配置文件的 testEnvironment 爲自定義環境 <rootDir>/test/@helpers/jest-env.js

編寫自定義環境 @helpers/jest-env.js

const NodeEnvironment = require('jest-environment-node')
const { MongoMemoryServer } = require('mongodb-memory-server')
const child_process = require('child_process')

// 繼承 Node 環境
class CustomEnvironment extends NodeEnvironment {
  // 在測試套件啓動前,獲取本地開發 MongoDB Uri 並注入 global 對象
  async setup() {
    const uri = await getMongoUri()
    this.global.testConfig = {
      mongo: { uri },
    }
    await super.setup()
  }
}

async function getMongoUri() {
  // 經過 which mongod 命令拿到本地 MongoDB 二進制文件路徑
  const mongodPath = await new Promise((resolve, reject) => {
    child_process.exec(
      'which mongod',
      { encoding: 'utf8' },
      (err, stdout, stderr) => {
        if (err || stderr) {
          return reject(
            new Error('找不到系統的 mongod,請確保 `which mongod` 能夠指向 mongod')
          )
        }
        resolve(stdout.trim())
      }
    )
  })

  // 使用本地 MongoDB 二進制文件建立內存服務實例
  const mongod = new MongoMemoryServer({
    binary: { systemBinary: mongodPath },
  })
  
  // 獲得建立成功的實例 Uri 地址
  const uri = await mongod.getConnectionString()
  return uri
}

// 導出自定義環境類
module.exports = CustomEnvironment

Mongoose 中即可以這樣鏈接:

await mongoose.connect((global as any).testConfig.mongo.uri, {
  useNewUrlParser: true,
  useUnifiedTopology: true,
})

固然在 package.json 裏須要禁用 MongoDB Memory Server 去下載二進制包,由於上面已經使用了本地二進制包。

"config": {
  "mongodbMemoryServer": {
    "version": "4.0",
    // 禁止在 yarn install 時下載二進制包
    "disablePostinstall": "1",
    "md5Check": "1"
  }
}

登陸功能封裝與使用

大多時候接口是須要登陸後才能訪問的,因此咱們須要把整塊登陸功能抽離出來,封裝成通用方法,同時藉此初始化一些測試專用數據。

爲了使 API 易用,我但願登陸 API 長這樣:

import { login } from '../@helpers/login'

// 調用登陸方法,根據傳遞的角色建立用戶,並返回該用戶登陸的 request 對象。
// 支持多參數,根據參數不一樣自動初始化測試數據。
const request = await login({
  role: 'user',
})

// 使用已登陸的 request 對象訪問須要登陸的用戶接口,
// 應當是登陸態,並正確返回當前登陸的用戶信息。
const res = await request.get('/api/user/info')

開發登陸方法:

// @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)
  
  // 若是沒有用戶名,自動建立用戶名
  if (!userData.username) {
    userData.username = chance.word({ length: 8 })
  }

  // 若是沒有暱稱,自動建立暱稱
  if (!userData.nickname) {
    userData.nickname = chance.word({ length: 8 })
  }

  // 獲得支持 cookie 持久化的 request 對象
  const request: any = makeAgent()

  // 發送登陸請求,這裏爲測試專門設計一個登陸接口
  // 包含正常登陸功能,但還會根據傳參不一樣初始化測試專用數據
  const res = await request.post('/api/login-test').send(userData)

  // 將登陸返回的數據賦值到 request 對象上
  assign(request, pick(res.body, ['user', 'otherValidKey...']))

  // 返回 request 對象
  return request as RequestAgent
}

實際用例中就像上面示例方式使用。

模擬功能使用

從項目架構中能夠看到項目也會調用較多外部服務。比方說建立文件夾的接口,內部代碼須要調用外部服務去鑑定文件夾名稱是否包含敏感詞,就像這樣:

import { detectText } from '~/utils/detect'

// 調用外部服務檢測文件夾名稱是否包含敏感詞
const { ok, sensitiveWords } = await detectText(folderName)
if (!ok) {
  throw new Error(`檢測到敏感詞: ${sensitiveWords}`)
}

實際測試的時候並不須要全部測試用例運行時都調用外部服務,這樣會拖慢測試用例的響應時間以及不穩定性。咱們能夠創建個更好的機制,新建一個測試套件專門用於驗證 detectText 工具方法的正確性,而其餘測試套件運行時 detectText 方法直接返回 OK 便可,這樣既保證了 detectText 方法被驗證到,也保證了其餘測試套件獲得快速響應。

模擬功能(Mock)就是爲這樣的情景而誕生的。咱們只須要在 detectText 方法的路徑 utils/detect.ts 同級新建__mocks__/detect.ts 模擬文件便可,內容以下,直接返回結果:

export async function detectText(
  text: string
): Promise<{ ok: boolean; sensitive: boolean; sensitiveWords?: string }> {
  // 刪除全部代碼,直接返回 OK
  return { ok: true, sensitive: false }
}

以後每一個須要模擬的測試套件頂部加上下面一句代碼便可。

jest.mock('~/utils/detect.ts')

在驗證 detectText 工具方法的測試套件裏,則只需 jest.unmock 便可恢復真實的方法。

jest.unmock('~/utils/detect.ts')

固然應該把 jest.mock 寫在 setupFiles 配置裏,由於須要模擬的測試套件佔絕大多數,寫在配置裏會讓它們在運行前自動加載該文件,這樣開發就沒必要每處測試套件都加上一段一樣的代碼,能夠有效提升開發效率。

// jest.config.js
setupFiles: ['<rootDir>/test/@helpers/jest-setup.ts']
// @helpers/jest-setup.ts
jest.mock('~/utils/detect.ts')

模擬功能還有方法模擬,定時器模擬等,能夠查閱其文檔瞭解更多示例。

快照功能使用

快照功能(Snapshot)能夠幫咱們測試大型對象,從而簡化測試用例。

舉個例子,項目的模板解析接口,該接口會將 PSD 模板文件進行解析,而後吐出一個較大的 JSON 數據,若是挨個校驗對象的屬性是否正確可能很不理想,因此可使用快照功能,就是第一次運行測試用例時,會把 JSON 數據存儲到本地文件,稱之爲快照文件,第二次運行時,就會將第二次返回的數據與快照文件進行比較,若是兩個快照匹配,則表示測試成功,反之測試失敗。

而使用方式很簡單:

// 請求模板解析接口
const res = await request.post('/api/secret/parser')

// 斷言快照是否匹配
expect(res.body).toMatchSnapshot()

更新快照也是敏捷的,運行命令 jest --updateSnapshot 或在監聽模式輸入 u 來更新。

集成測試

集成測試的概念是在單元測試的基礎上,將全部模塊按照必定要求或流程關係進行串聯測試。比方說,一些模塊雖然可以單獨工做,但並不能保證鏈接起來也能正常工做,一些局部反映不出來的問題,在全局上極可能暴露出來。

由於測試框架 Jest 對於每一個測試套件是並行運行的,而套件內的用例則是串行運行的,因此編寫集成測試很方便,下面咱們用文件夾的使用流程示例如何完成集成測試的編寫。

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('沒有權限建立文件夾應該返回 403 錯誤', async () => {
  const res = await request.post(urlCreateFolder).send({
    name: '個人文件夾',
  })
  expect(res.status).toBe(403)
})

test('確保建立 3 個文件夾', async () => {
  // 登陸有權限建立文件夾的用戶,好比設計師
  globalReq = await login({ role: 'designer' })
  
  for (let i = 0; i < 3; i++) {
    const res = await globalReq.post(urlCreateFolder).send({
      name: '個人文件夾' + i,
    })
    
    // 將建立成功的文件夾置入 folders 常量裏
    folders.push(res.body)
    
    expect(res.status).toBe(200)
    // 更多驗證規則...
  }
})

test('重命名第 2 個文件夾', async () => {
  const res = await globalReq.put(urlRenameFolder).send({
    id: folders[1].id,
    name: '新文件夾名稱',
  })
  expect(res.status).toBe(200)
})

test('第 2 個文件夾的名稱應該是【新文件夾名稱】', async () => {
  const res = await globalReq.get(urlFolderDetails).query({
    id: folders[1].id,
  })
  expect(res.status).toBe(200)
  expect(res.body.name).toBe('新文件夾名稱')
  // 更多驗證規則...
})

test('獲取文件夾列表應該返回 3 條數據', async () => {
  // 與上雷同,鑑於代碼過多,先行省略...
})

test('刪除最後一個文件夾', async () => {
  // 與上雷同,鑑於代碼過多,先行省略...
})

test('再次獲取文件夾列表應該返回 2 條數據', async () => {
  // 與上雷同,鑑於代碼過多,先行省略...
})

測試覆蓋率

測試覆蓋率是對測試完成程度的評測,基於文件被測試的狀況來反饋測試的質量。

運行命令 jest --coverage 便可生成測試覆蓋率報告,打開生成的 coverage/lcov-report/index.html 文件,各項指標盡收眼底。由於 Jest 內部使用 Istanbul 生成覆蓋率報告,因此各項指標依然參考 Istanbul

測試覆蓋率報告

持續集成

寫完這麼多測試用例以後,或者是開發完功能代碼後,咱們是否是但願每次將代碼推送到託管平臺,如 GitLab,託管平臺能自動幫咱們運行全部測試用例,若是測試失敗就郵件通知咱們修復,若是測試經過則把開發分支合併到主分支?

答案是必須的。這就與持續集成(Continuous Integration)不謀而合,通俗的講就是常常性地將代碼合併到主幹分支,每次合併前都須要運行自動化測試以驗證代碼的正確性。

因此咱們配置一些自動化測試任務,按順序執行安裝、編譯、測試等命令,測試命令則是運行編寫好的測試用例。一個 GitLab 的配置任務(.gitlab-ci.yml)可能像下面這樣,僅做參考。

# 每一個 job 以前執行的命令
before_script:
  - echo "`whoami` ($0 $SHELL)"
  - echo "`which node` (`node -v`)"
  - echo $CI_PROJECT_DIR

# 定義 job 所屬 test 階段及執行的命令等
test:
  stage: test
  except:
    - test
  cache:
    paths:
      - node_modules/
  script:
    - yarn
    - yarn lint
    - yarn test

# 定義 job 所屬 deploy 階段及執行的命令等
deploy-stage:
  stage: deploy
  only:
    - test
  script:
    - cd /app 
    - make BRANCH=origin/${CI_COMMIT_REF_NAME} deploy-stage

持續集成的好處:

  1. 快速發現錯誤。
  2. 防止分支大幅偏離主幹分支。
  3. 讓產品能夠快速迭代,同時還能保持高質量。

TDD與BDD引入

TDD 全稱測試驅動開發(Test-driven development),是敏捷開發中的一種設計方法論,強調先將需求轉換爲具體的測試用例,而後再開發代碼以使測試經過。

BDD 全稱行爲驅動開發(Behavior-driven development),也是一種敏捷開發設計方法論,它沒有強調具體的形式如何,而是強調【做爲何角色,想要什麼功能,以便收益什麼】這樣的用戶故事指定行爲的論點。

二者都是很好的開發模式,結合實際狀況,咱們的測試更像是 BDD,不過並無徹底摒棄 TDD,咱們的建議是若是以爲先寫測試能夠幫助更快的寫好代碼,那就先寫測試,若是以爲先寫代碼再寫測試,或一邊開發一邊測試更好,則採用本身的方式,而結果是編碼功能和測試用例都須要完成,而且運行經過,最後經過 Code Review 對代碼質量作進一步審查與把控。

筆者稱之爲【師夷長技,聚於自身】:結合項目自身的實際狀況,靈活變通,造成一套適合自身項目發展的模式驅動開發。

結論

自動化測試提供了一種有保障的機制檢測整個系統,能夠頻繁地進行迴歸測試,有效提升系統穩定性。固然編寫與維護測試用例須要耗費必定的成本,須要考慮投入與產出效益之間的平衡。

歡迎關注凹凸實驗室博客:aotu.io

或者關注凹凸實驗室公衆號(AOTULabs),不定時推送文章:

13-橫_1575377567139.jpg

相關文章
相關標籤/搜索