全棧測試實戰:用Jest測試Vue+Koa全棧應用

NO IMAGE

本文首發於我的博客,歡迎踩點~

前言

今年一月份的時候我寫了一個Vue+Koa的全棧應用,以及相應的配套教程,得到了很多的好評。同時我也在和讀者交流的過程中不斷認識到不足和缺點,於是也對此進行了不斷的更新和完善。本次帶來的完善是加入和完整的前後端測試。相信對於很多學習前端的朋友來說,測試這個東西似乎是個熟悉的陌生人。你聽過,但是你未必做過。如果你對前端(以及nodejs端)測試很熟悉,那麼本文的幫助可能不大,不過我很希望能得到你們提出的寶貴意見!

簡介

和上一篇全棧開發實戰:用Vue2+Koa1開發完整的前後端項目一樣,本文從測試新手的角度出發(默認了解Koa並付諸實踐,瞭解Vue並付諸實踐,但是並無測試經歷),在已有的項目上從0開始構建我們的全棧測試系統。可以瞭解到測試的意義,Jest測試框架的搭建,前後端測試的異同點,如何寫測試用例,如何查看測試結果並提升我們的測試覆蓋率,100%測試覆蓋率是否是必須,以及在搭建測試環境、以及測試本身過程中遇到的各種疑難雜症。希望可以作為入門前端以及Node端測試的文章吧。

項目結構

有了之前的項目結構作為骨架,加入Jest測試框架就很簡單了。

.
├── LICENSE
├── README.md
├── .env  // 環境變量配置文件
├── app.js  // Koa入口文件
├── build // vue-cli 生成,用於webpack監聽、構建
│   ├── build.js
│   ├── check-versions.js
│   ├── dev-client.js
│   ├── dev-server.js
│   ├── utils.js
│   ├── webpack.base.conf.js
│   ├── webpack.dev.conf.js
│   └── webpack.prod.conf.js
├── config // vue-cli 生成&自己加的一些配置文件
│   ├── default.conf
│   ├── dev.env.js
│   ├── index.js
│   └── prod.env.js
├── dist // Vue build 後的文件夾
│   ├── index.html // 入口文件
│   └── static // 靜態資源
├── env.js // 環境變量切換相關 <-- 新
├── .env // 開發、上線時的環境變量 <-- 新
├── .env.test // 測試時的環境變量 <-- 新
├── index.html // vue-cli生成,用於容納Vue組件的主html文件。單頁應用就只有一個html
├── package.json // npm的依賴、項目信息文件、Jest的配置項 <-- 新
├── server // Koa後端,用於提供Api
│   ├── config // 配置文件夾
│   ├── controllers // controller-控制器
│   ├── models // model-模型
│   ├── routes // route-路由
│   └── schema // schema-數據庫表結構
├── src // vue-cli 生成&自己添加的utils工具類
│   ├── App.vue // 主文件
│   ├── assets // 相關靜態資源存放
│   ├── components // 單文件組件
│   ├── main.js // 引入Vue等資源、掛載Vue的入口js
│   └── utils // 工具文件夾-封裝的可複用的方法、功能
├── test
│   ├── sever // 服務端測試 <-- 新
│   └── client // 客戶端(前端)測試 <-- 新
└── yarn.lock // 用yarn自動生成的lock文件

可以看到新增的或者說更新的東西只有幾個:

  1. 最主要的test文件夾,包含了客戶端(前端)和服務端的測試文件
  2. env.js以及配套的.env.env.test,是跟測試相關的環境變量
  3. package.json,更新了一些依賴以及Jest的配置項

主要環境:Vue2,Koa2,Nodejs v8.9.0

測試用到的一些關鍵依賴

以下依賴的版本都是本文所寫的時候的版本,或者更舊一些

  1. jest: ^21.2.1
  2. babel-jest: ^21.2.0
  3. supertest: ^3.0.0
  4. dotenv: ^4.0.0

剩下依賴可以項目demo倉庫

搭建Jest測試環境

對於測試來說,我也是個新手。至於為什麼選擇了Jest,而不是其他框架(例如mocha+chai、jasmine等),我覺得有如下我自己的觀點(當然你也可以不採用它):

  1. 由Facebook開發,保證了更新速度以及框架質量
  2. 它有很多集成的功能(比如斷言庫、比如測試覆蓋率)
  3. 文檔完善,配置簡單
  4. 支持typescript,我在學習typescript的時候也用了Jest來寫測試
  5. Vue官方的單元測試框架vue-test-utils專門有配合Jest的測試說明
  6. 支持快照功能,對前端單元測試是一大利好
  7. 如果你是React技術棧,Jest天生就適配React

安裝

yarn add jest -D
#or
npm install jest --save-dev

很簡單對吧。

配置

由於我項目的Koa後端用的是ES modules的寫法而不是Nodejs的Commonjs的寫法,所以是需要babel的插件來進行轉譯的。否則你運行測試用例的時候,將會出現如下問題:

 ● Test suite failed to run
/Users/molunerfinn/Desktop/work/web/vue-koa-demo/test/sever/todolist.test.js:1
({"Object.<anonymous>":function(module,exports,require,__dirname,__filename,global,jest){import _regeneratorRuntime from 'babel-runtime/regenerator';import _asyncToGenerator from 'babel-runtime/helpers/asyncToGenerator';var _this = this;import server from '../../app.js';
^^^^^^
SyntaxError: Unexpected token import
at ScriptTransformer._transformAndBuildScript (node_modules/jest-runtime/build/script_transformer.js:305:17)
at Generator.next (<anonymous>)
at new Promise (<anonymous>)

看了官方github的README發現應該是babel-jest沒裝。

yarn add babel-jest -D
#or
npm install babel-jest --save-dev

但是奇怪的是,文檔裡說:Note: babel-jest is automatically installed when installing Jest and will automatically transform files if a babel configuration exists in your project. 也就是babel-jest在jest安裝的時候便會自動安裝了。這點需要求證。

然而發現運行測試用例的時候還是出了上述問題,查閱了相關issue之後,我給出兩種解決辦法:

都是修改項目目錄下的.babelrc配置文件,增加env屬性,配置test環境如下:

1. 增加presets

"env": {
"test": {
"presets": ["env", "stage-2"] // 採用babel-presents-env來轉譯
}
}

2. 或者增加plugins

"env": {
"test": {
"plugins": ["transform-es2015-modules-commonjs"] // 採用plugins來講ES modules轉譯成Commonjs modules
}
}

再次運行,編譯通過。

通常我們將測試文件(*.test.js或*.spec.js)放置在項目的test目錄下。Jest將會自動運行這些測試用例。值得一提的是,通常我們將基於TDD的測試文件命名為*.test.js,把基於BDD的測試文件命名為*.spec.js。這二者的區別可以看這篇文章

我們可以在package.jsonscripts字段里加入test的命令(如果原本存在則換一個名字,不要衝突)

"scripts": {
// ...其他命令
"test": "jest"
// ...其他命令
},

這樣我們就可以在終端直接運行npm test來執行測試了。下面我們先來從後端的Api測試開始寫起。

Koa後端Api測試

重現一下之前的應用的操作流程,可以發現應用分為登錄前和登錄後兩種狀態。

全棧測試實戰:用Jest測試Vue+Koa全棧應用

可以根據操作流程或者後端api的結構來寫測試。如果根據操作流程來寫測試就可以分為登錄前和登錄後。如果根據後端api的結構的話,就可以根據routes或者controllers的結構、功能來寫測試。

由於本例登錄前和登錄後的api基本上是分開的,所以我主要根據上述後者(routes或controllers)來寫測試。

到此需要解釋一下一般來說(寫)測試的步驟:

  1. 寫測試說明,針對你的每條測試說明測試了什麼功能,預期結果是什麼。
  2. 寫測試主體,通常是 輸入 -> 輸出。
  3. 判斷測試結果,拿輸出和預期做對比。如果輸出和預期相符,則測試通過。反之,不通過。

test文件夾下新建一個server文件夾。然後創建一個user.spec.js文件。

我們可以通過

import server from '../../app.js'

的方式將我們的Koa應用的主入口文件引入。但是此時遇到了一個問題。我們如何對這個server發起http請求,並對其的返回結果做出判斷呢?

在閱讀了Async testing Koa with Jest以及A clear and concise introduction to testing Koa with Jest and Supertest這兩篇文章之後,我決定使用supertest這個工具了。它是專門用來測試nodejs端HTTP server的測試工具。它內封了superagent這個著名的Ajax請求庫。並且支持Promise,意味著我們對於異步請求的結果也能通過async await的方式很好的控制了。

安裝:

yarn add supertest -D
#or
npm install supertest --save-dev

現在開始著手寫我們第一個測試用例。先寫一個針對登錄功能的吧。當我們輸入了錯誤的用戶名或者密碼的時候將無法登錄,後端返回的參數裡,success會是false。

// test/server/user.spec.js
import server from '../../app.js'
import request from 'supertest'
afterEach(() => {
server.close() // 當所有測試都跑完了之後,關閉server
})
// 如果輸入用戶名為Molunerfinn,密碼為1234則無法登錄。正確應為molunerfinn和123。
test('Failed to login if typing Molunerfinn & 1234', async () => { // 注意用了async
const response = await request(server) // 注意這裡用了await
.post('/auth/user') // post方法向'/auth/user'發送下面的數據
.send({
name: 'Molunerfinn',
password: '1234'
})
expect(response.body.success).toBe(false) // 期望回傳的body的success值是false(代表登錄失敗)
})

上述例子中,test()方法能接受3個參數,第一個是對測試的描述(string),第二個是回調函數(fn),第三個是延時參數(number)。本例不需要延時。然後expect()函數裡放輸出,再用各種match方法來將預期和輸出做對比。

在終端執行npm test,緊張地希望能跑通也許是人生的第一個測試用例。結果我得到如下關鍵的報錯信息:

 ● Post todolist failed if not give the params
TypeError: app.address is not a function
...
● Post todolist failed if not give the params
TypeError: _app2.default.close is not a function

這是怎麼回事?說明我們import進來的server看來並沒有close、address等方法。原因在於我們在app.js裡最後一句:

export default app

此處export出來的是一個對象。但我們實際上需要一個function。

在谷歌的過程中,找到兩種解決辦法:

參考解決辦法1解決辦法2

1. 修改app.js

app.listen(8889, () => {
console.log(`Koa is listening in 8889`)
})
export default app

改為

export default app.listen(8889, () => {
console.log(`Koa is listening in 8889`)
})

即可。

2. 修改你的test文件:

在裡要用到server的地方都改為server.callback()

const response = await request(server.callback())
.post('/auth/user')
.send({
name: 'Molunerfinn',
password: '1234'
})

我採用的是第一種做法。

改完之後,順利通過:

 PASS  test/sever/user.test.js
✓ Failed to login if typing Molunerfinn & 1234 (248ms)

然而此時發現一個問題,為何測試結束了,jest還佔用著終端進程呢?我想要的是測試完jest就自動退出了。查了一下文檔,發現它的cli有個參數--forceExit能解決這個問題,於是就把package.json裡的test命令修改一下(後續我們還將修改幾次)加上這個參數:

"scripts": {
// ...其他命令
"test": "jest --forceExit"
// ...其他命令
},

再測試一遍,發現沒問題。這樣一來我們就可以繼續依葫蘆畫瓢,把auth/*這個路由的功能都測試一遍:

// server/routes/auth.js
import auth from '../controllers/user.js'
import koaRouter from 'koa-router'
const router = koaRouter()
router.get('/user/:id', auth.getUserInfo) // 定義url的參數是id
router.post('/user', auth.postUserAuth)
export default router

測試用例如下:

import server from '../../app.js'
import request from 'supertest'
afterEach(() => {
server.close()
})
test('Failed to login if typing Molunerfinn & 1234', async () => {
const response = await request(server)
.post('/auth/user')
.send({
name: 'Molunerfinn',
password: '1234'
})
expect(response.body.success).toBe(false)
})
test('Successed to login if typing Molunerfinn & 123', async () => {
const response = await request(server)
.post('/auth/user')
.send({
name: 'Molunerfinn',
password: '123'
})
expect(response.body.success).toBe(true)
})
test('Failed to login if typing MARK & 123', async () => {
const response = await request(server)
.post('/auth/user')
.send({
name: 'MARK',
password: '123'
})
expect(response.body.info).toBe('用戶不存在!')
})
test('Getting the user info is null if the url is /auth/user/10', async () => {
const response = await request(server)
.get('/auth/user/10')
expect(response.body).toEqual({})
})
test('Getting user info successfully if the url is /auth/user/2', async () => {
const response = await request(server)
.get('/auth/user/2')
expect(response.body.user_name).toBe('molunerfinn')
})

都很簡潔易懂,看描述+預期你就能知道在測試什麼了。不過需要注意一點的是,我們用到了toBe()toEqual()兩個方法。乍一看好像沒有區別。實際上有大區別。

簡單來說,toBe()適合===這個判斷條件。比如1 === 1'hello' === 'hello'。但是[1] === [1]是錯的。具體原因不多說,js的基礎。所以要判斷比如數組或者對象相等的話需要用toEqual()這個方法。

OK,接下去我們開始測試api/*這個路由。

test目錄下創建一個叫做todolits.spec.js的文件:

有了上一個測試的經驗,測試這個其實也不會有多大的問題。首先我們來測試一下當我們沒有攜帶上JSON WEB TOKEN的header的話,服務端是不是返回401錯誤:

import server from '../../app.js'
import request from 'supertest'
afterEach(() => {
server.close()
})
test('Getting todolist should return 401 if not set the JWT', async () => {
const response = await request(server)
.get('/api/todolist/2')
expect(response.status).toBe(401)
})

一切看似沒問題,但是運行的時候卻報錯了:

console.error node_modules/jest-jasmine2/build/jasmine/Env.js:194
Unhandled error
console.error node_modules/jest-jasmine2/build/jasmine/Env.js:195
Error: listen EADDRINUSE :::8888
at Object._errnoException (util.js:1024:11)
at _exceptionWithHostPort (util.js:1046:20)
at Server.setupListenHandle [as _listen2] (net.js:1351:14)
at listenInCluster (net.js:1392:12)
at Server.listen (net.js:1476:7)
at Application.listen (/Users/molunerfinn/Desktop/work/web/vue-koa-demo/node_modules/koa/lib/application.js:64:26)
at Object.<anonymous> (/Users/molunerfinn/Desktop/work/web/vue-koa-demo/app.js:60:5)
at Runtime._execModule (/Users/molunerfinn/Desktop/work/web/vue-koa-demo/node_modules/jest-runtime/build/index.js:520:13)
at Runtime.requireModule (/Users/molunerfinn/Desktop/work/web/vue-koa-demo/node_modules/jest-runtime/build/index.js:332:14)
at Runtime.requireModuleOrMock (/Users/molunerfinn/Desktop/work/web/vue-koa-demo/node_modules/jest-runtime/build/index.js:408:19)

看來是因為同時運行了兩個Koa實例導致了監聽端口的衝突。所以我們需要讓Jest按順序執行。查閱官方文檔,發現了runInBand這個參數正是我們想要的。

所以修改package.json裡的test命令如下:

"scripts": {
// ...其他命令
"test": "jest --forceExit --runInBand"
// ...其他命令
},

再次運行,成功通過!

接下來遇到一個問題。我們的JWT的token原本是登錄成功後生成並派發給前端的。如今我們測試api的時候並沒有經過登錄那一步。所以要測試的時候要用的token的話,我覺得有兩種辦法:

  1. 增加測試的時候的api接口,不需要經過koa-jwt的驗證。但是這種方法對項目有入侵性的影響,如果有的時候我們需要從token獲取信息的話就有問題了。
  2. 後端預先生成一個合法的token,然後測試的時候用上這個測試的token即可。不過這種辦法的話就需要保證token不能洩露。

我採用第二種辦法。為了讀者使用方便我是預先生成一個token然後用一個變量存起來的。(真正的開發環境下應對將測試的token放置在項目環境變量.env中)

接下來我們測試一下數據庫的四大操作:增刪改查。不過我們為了一次性將這四個接口都測試一遍可以按照這個順序:增查改刪。其實就是先增加一個todo,然後查找的時候將id記錄下來。隨後可以用這個id進行更新和刪除。

import server from '../../app.js'
import request from 'supertest'
afterEach(() => {
server.close()
})
const token = 'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYW1lIjoibW9sdW5lcmZpbm4iLCJpZCI6MiwiaWF0IjoxNTA5ODAwNTg2fQ.JHHqSDNUgg9YAFGWtD0m3mYc9-XR3Gpw9gkZQXPSavM' // 預先生成的token
let todoId = null // 用來存放測試生成的todo的id
test('Getting todolist should return 401 if not set the JWT', async () => {
const response = await request(server)
.get('/api/todolist/2')
expect(response.status).toBe(401)
})
// 增
test('Created todolist successfully if set the JWT & correct user', async () => { 
const response = await request(server)
.post('/api/todolist')
.send({
status: false,
content: '來自測試',
id: 2
})
.set('Authorization', 'Bearer ' + token) // header處加入token驗證
expect(response.body.success).toBe(true)
})
// 查
test('Getting todolist successfully if set the JWT & correct user', async () => {
const response = await request(server)
.get('/api/todolist/2')
.set('Authorization', 'Bearer ' + token)
response.body.result.forEach((item, index) => {
if (item.content === '來自測試') todoId = item.id // 獲取id
})
expect(response.body.success).toBe(true)
})
// 改
test('Updated todolist successfully if set the JWT & correct todoId', async () => {
const response = await request(server)
.put(`/api/todolist/2/${todoId}/0`) // 拿id去更新
.set('Authorization', 'Bearer ' + token)
expect(response.body.success).toBe(true)
})
// 刪
test('Removed todolist successfully if set the JWT & correct todoId', async () => {
const response = await request(server)
.delete(`/api/todolist/2/${todoId}`)
.set('Authorization', 'Bearer ' + token)
expect(response.body.success).toBe(true)
})

對照著api的4大接口,我們已經將它們都測試了一遍。那是不是我們對於服務端的測試已經結束了呢?其實不是的。要想保證後端api的健壯性,我們得將很多情況都考慮到。但是人為的去排查每個條件、語句什麼的必然過於繁瑣和機械。於是我們需要一個指標來幫我們確保測試的全面性。這就是測試覆蓋率了。

後端api測試覆蓋率

上面說過,Jest是自帶了測試覆蓋率功能的(其實就是基於istanbul這個工具來生成測試覆蓋率的)。要如何開啟呢?這裡我還走了不少坑。

通過閱讀官方的配置文檔,我確定了幾個需要開啟的參數:

  1. coverageDirectory,指定輸出測試覆蓋率報告的目錄
  2. coverageReporters,指定輸出的測試覆蓋率報告的形式,具體可以參考istanbul的說明
  3. collectCoverage,是否要收集覆蓋率信息,當然是。
  4. mapCoverage,由於我們的代碼經過babel-jest轉譯,所以需要開啟sourcemap來讓Jest能夠把測試結果定位到源代碼上而不是編譯的代碼上。
  5. verbose,用於顯示每個測試用例的通過與否。

於是我們需要在package.json裡配置一個Jest字段(不是在scripts字段裡配置,而是和scripts在同一級的字段),來配置Jest。

配置如下:

"jest": {
"verbose": true,
"coverageDirectory": "coverage",
"mapCoverage": true,
"collectCoverage": true,
"coverageReporters": [
"lcov", // 會生成lcov測試結果以及HTML格式的漂亮的測試覆蓋率報告
"text" // 會在命令行界面輸出簡單的測試報告
]
}

然後我們再進行一遍測試,可以看到在終端裡已經輸出了簡易的測試報告總結:

全棧測試實戰:用Jest測試Vue+Koa全棧應用

從中我們能看到一些字段是100%,而一些不是100%。最後一列Uncovered Lines就是告訴我們,測試裡沒有覆蓋到的代碼行。為了更直觀地看到測試的結果報告,可以到項目的根目錄下找到一個coverage的目錄,在lcov-report目錄裡有個index.html就是輸出的html報告。打開來看看:

全棧測試實戰:用Jest測試Vue+Koa全棧應用

首頁是個概覽,跟命令行裡輸出的內容差不多。不過我們可以往深了看,可以點擊左側的File提供的目錄:

全棧測試實戰:用Jest測試Vue+Koa全棧應用

然後我們可以看到沒有被覆蓋到代碼行數(50)以及有一個函數沒有被測試到:

全棧測試實戰:用Jest測試Vue+Koa全棧應用

通常我們沒有測試到的函數也伴隨著代碼行數沒有被測試到。我們可以看到在本例裡,app的error事件沒有被觸發過。想想也是的,我們的測試都是建立在合法的api請求的基礎上的。所以自然不會觸發error事件。因此我們需要寫一個測試用例來測試這個.on('error')的函數。

全棧測試實戰:用Jest測試Vue+Koa全棧應用

通常這樣的測試用例並不是特別好寫。不過好在我們可以嘗試去觸發server端的錯誤,對於本例來說,如果向服務端創建一個todo的時候,沒有附上相應的信息(id、status、content),就無法創建相應的todo,會觸發錯誤。


// server/models/todolist.js
const createTodolist = async function (data) {
await Todolist.create({
user_id: data.id,
content: data.content,
status: data.status
})
return true
}

上面是server端創建todo的相關函數,下面是針對它的錯誤進行的測試:

// test/server/todolist.spec.js
// ...
test('Failed to create a todo if not give the params', async () => {
const response = await request(server)
.post('/api/todolist')
.set('Authorization', 'Bearer ' + token) // 不發送創建的參數
expect(response.status).toBe(500) // 服務端報500錯誤
})

再進行測試,發現之前對於app.js的相關測試都已經是100%了。

全棧測試實戰:用Jest測試Vue+Koa全棧應用

不過controllers/todolist.js裡還是有未測試到的行數34,以及我們可以看到% Branch這列的數字顯示的是50而不是100。Branch的意思就是分支測試。什麼是分支測試呢?簡單來說就是你的條件語句測試。比如一個if...else語句,如果測試用例只跑過if的條件,而沒有跑過else的條件,那麼Branch的測試就不完整。讓我們來看看是什麼條件沒有測試到?

全棧測試實戰:用Jest測試Vue+Koa全棧應用

可以看到是個三元表達式並沒有測試完整。(三元表達式也算分支)我們測試了0的情況,但是沒有測試非零的情況,所以再寫一個非零的情況:

test('Failed to update todolist  if not update the status of todolist', async () => {
const response = await request(server)
.put(`/api/todolist/2/${todoId}/1`) // <- 這裡最後一個參數改成了1
.set('Authorization', 'Bearer ' + token)
expect(response.body.success).toBe(false)
})

再次跑測試:

全棧測試實戰:用Jest測試Vue+Koa全棧應用

哈,成功做到了100%測試覆蓋率!

端口占用和環境變量的引入

雖然做到了100%測試覆蓋率,但是有一個問題卻是不容忽視的。那就是我們現在測試環境和開發環境下的服務端監聽的端口是一致的。意味著你不能在開發環境下測試你的代碼。比如你寫完一個api之後馬上要寫一個測試用例的時候,如果測試環境和開發環境的服務端監聽的端口一致的話,測試的時候就會因為端口被佔用而無法被監聽到。

所以我們需要指定一下測試環境下的端口,讓它和開發乃至生產環境的端口不一樣。我一開始想法很簡單,指定一下NODE_ENV=test的時候用8888端口,開發環境下用8889端口。在app.js裡就是這樣寫:

// ...
let port = process.env.NODE_ENV === 'test' ? 8888 : 8889
// ...
export default app.listen(port, () => {
console.log(`Koa is listening in ${port}`)
})

接下去就遇到了兩個問題:

  1. 需要解決跨平臺env設置
  2. 這樣設置的話一旦在測試環境下,對於port這句話,Branch測試是無法完全通過的——因為始終是在test環境下,無法運行到port = 8889那個條件

跨平臺env設置

跨平臺env主要涉及到windows、linux和macOS。要在三個平臺在測試的時候都跑著NODE_ENV=test的話,我們需要藉助cross-env來幫助我們。

yarn add cross-env -D
#or
npm install cross-env --save-dev

然後在package.json裡修改test的命令如下:

"scripts": {
// ...其他命令
"test": "cross-env NODE_ENV=test jest --forceExit --runInBand"
// ...其他命令
},

這樣就能在後端代碼裡,通過process.env.NODE_ENV這個變量訪問到test這個值。這樣就解決了第一個問題。

端口分離並保證測試覆蓋率

目前為止,我們已經能夠解決測試環境和開發環境的監聽端口一致的問題了。不過卻帶來了測試覆蓋率不全的問題。

為此我找到兩種解決辦法:

  1. 通過istanbul特殊的ignore註釋來忽略測試環境下的一些測試分支條件
  2. 通過配置環境變量文件,不同環境下采用不同的環境變量文件

第一種方法很簡單,在需要忽略的地方,輸入/* istanbul ignore next *//* istanbul ignore <word>[non-word] [optional-docs] */等語法忽略代碼。不過考慮到這是涉及到測試環境和開發環境下的環境變量問題,如果不僅僅是端口問題的話,那麼就不如採用第二種方法來得更加優雅。(比如開發環境和測試環境的數據庫用戶和密碼都不一樣的話,還是需要寫在對應的環境變量的)

此時我們需要另外一個很常用的庫dotenv,它能默認讀取.env文件裡的值,讓我們的項目可以通過不同的.env文件來應對不同的環境要求。

步驟如下:

1. 安裝dotenv
yarn add dotenv
#or
npm install dotenv --save
2. 在項目根目錄下創建.env.env.test兩個文件,分別應用於開發環境和測試環境

// .env

DB_USER=xxxx # 數據庫用戶
DB_PASSWORD=yyyy # 數據庫密碼
PORT=8889 # 監聽端口

// .env.test

DB_USER=xxxx # 數據庫用戶
DB_PASSWORD=yyyy # 數據庫密碼
PORT=8888 # 監聽端口
3. 創建一個env.js文件,用於不同環境下采用不同的環境變量。代碼如下:
import * as dotenv from 'dotenv'
let path = process.env.NODE_ENV === 'test' ? '.env.test' : '.env'
dotenv.config({path, silent: true})
4. 在app.js開頭引入env
import './env'

然後把原本那句port的話改成:

let port = process.env.PORT

再把數據庫連接的用戶密碼也用環境變量來代替:

// server/config/db.js
import '../../env'
import Sequelize from 'sequelize'
const Todolist = new Sequelize(`mysql://${process.env.DB_USER}:${process.env.DB_PASSWORD}@localhost/todolist`, {
define: {
timestamps: false // 取消Sequelzie自動給數據表加入時間戳(createdAt以及updatedAt)
}
})

不過需要注意的是,.env和.env.js文件都不應該納入git版本庫,因為都是比較重要的內容。

這樣就能實現不同環境下用不同的變量了。慢著!這樣不是還沒有解決問題嗎?env.js裡的條件還是無法被測試覆蓋啊——你肯定有這樣的疑問。不用緊張,現在給出解決辦法——給Jest指定收集測試覆蓋率的範圍:

修改package.jsonjest字段如下:

"jest": {
"verbose": true,
"coverageDirectory": "coverage",
"mapCoverage": true,
"collectCoverage": true,
"coverageReporters": [
"lcov",
"text"
],
"collectCoverageFrom": [ // 指定Jest收集測試覆蓋率的範圍
"!env.js", // 排除env.js
"server/**/*.js",
"app.js"
]
}

做完這些工作之後,再跑一次測試,一次通過:

全棧測試實戰:用Jest測試Vue+Koa全棧應用

這樣我們就完成了後端的api測試。完成了100%測試覆蓋率。下面我們可以開始測試Vue的前端項目了。

Vue前端測試

Vue的前端測試我就要推薦來自官方的vue-test-utils了。當然前端測試大致分成了單元測試(Unit test)和端對端測試(e2e test),由於端對端的測試對於測試環境的要求比較嚴苛,而且測試起來比較繁瑣,而且官方給出的測試框架是單元測試框架,因此本文對於Vue的前端測試也僅介紹配合官方工具的單元測試。

在Vue的前端測試中我們能夠瞭解到jest的mock、snapshot等特性和用法和vue-test-utils提供的mount、shallow、setData等一系列操作。

安裝vue-test-utils

根據官網的介紹我們需要安裝如下:

yarn add vue-test-utils vue-jest jest-serializer-vue -D
#or
npm install vue-test-utils vue-jest jest-serializer-vue --save-dev

其中,vue-test-utils是最關鍵的測試框架。提供了一系列對於Vue組件的測試操作。(下面會提到)。vue-jest用於處理*.vue的文件,jest-serializer-vue用於快照測試提供快照序列化。

配置vue-test-utils以及jest

1. 修改.babelrc

testenv裡增加或修改presets

{
"presets": [
["env", { "modules": false }],
"stage-2"
],
"plugins": [
"transform-runtime"
],
"comments": false,
"env": {
"test": {
"plugins": ["transform-es2015-modules-commonjs"],
"presets": [
["env", { "targets": { "node": "current" }}] // 增加或修改
]
}
}
}

2. 修改package.json裡的jest配置:

"jest": {
"verbose": true,
"moduleFileExtensions": [
"js"
],
"transform": { // 增加transform轉換
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest",
"^.+\\.js$": "<rootDir>/node_modules/babel-jest"
},
"coverageDirectory": "coverage",
"mapCoverage": true,
"collectCoverage": true,
"coverageReporters": [
"lcov",
"text"
],
"moduleNameMapper": { // 處理webpack alias
"@/(.*)$": "<rootDir>/src/$1"
},
"snapshotSerializers": [ // 配置快照測試
"<rootDir>/node_modules/jest-serializer-vue"
],
"collectCoverageFrom": [
"!env.js",
"server/**/*.js",
"app.js"
]
}

前端單元測試的一些說明

關於vue-test-utils和Jest的配合測試,我推薦可以查看這個系列的文章,講解很清晰。

接著,明確一下前端單元測試都需要測試些什麼東西。引用vue-test-utils的說法:

對於 UI 組件來說,我們不推薦一味追求行級覆蓋率,因為它會導致我們過分關注組件的內部實現細節,從而導致瑣碎的測試。

取而代之的是,我們推薦把測試撰寫為斷言你的組件的公共接口,並在一個黑盒內部處理它。一個簡單的測試用例將會斷言一些輸入 (用戶的交互或 prop 的改變) 提供給某組件之後是否導致預期結果 (渲染結果或觸發自定義事件)。

比如,對於每次點擊按鈕都會將計數加一的 Counter 組件來說,其測試用例將會模擬點擊並斷言渲染結果會加 1。該測試並沒有關注 Counter 如何遞增數值,而只關注其輸入和輸出。

該提議的好處在於,即便該組件的內部實現已經隨時間發生了改變,只要你的組件的公共接口始終保持一致,測試就可以通過。

所以,相對於後端api測試看重測試覆蓋率而言,前端的單元測試是不必一味追求測試覆蓋率的。(當然你要想達到100%測試覆蓋率也是沒問題的,只不過如果要達到這樣的效果你需要撰寫非常多繁瑣的測試用例,佔用太多時間,得不償失。)替代地,我們只需要迴歸測試的本源:給定輸入,我只關心輸出,不考慮內部如何實現。只要能覆蓋到和用戶相關的操作,能測試到頁面的功能即可。

和之前類似,我們在test/client目錄下書寫我們的測試用例。對於Vue的單元測試來說,我們就是針對*.vue文件進行測試了。由於本例裡的app.vue無實際意義,所以就測試Login.vueTodolist.vue即可。

運用vue-test-utils如何來進行測試呢?簡單來說,我們需要的做的就是用vue-test-utils提供的mount或者shallow方法將組件在後端渲染出來,然後通過一些諸如setDatapropsDatasetMethods等方法模擬用戶的操作或者模擬我們的測試條件,最後再用jest提供的expect斷言來對預期的結果進行判斷。這裡的預期就很豐富了。我們可以通過判斷事件是否觸發、元素是否存在、數據是否正確、方法是否被調用等等來對我們的組件進行比較全面的測試。下面的例子裡也會比較完整地介紹它們。

Login.vue的測試

創建一個login.spec.js文件。

首先我們來測試頁面裡是否有兩個輸入框和一個登錄按鈕。根據官方文檔,我首先注意到了shallow rendering,它的說明是,對於某個組件而言,只渲染這個組件本身,而不渲染它的子組件,讓測試速度提高,也符合單元測試的理念。看著好像很不錯的樣子,拿過來用。

查找元素測試

import { shallow } from 'vue-test-utils'
import Login from '../../src/components/Login.vue'
let wrapper
beforeEach(() => {
wrapper = shallow(Login) // 每次測試前確保我們的測試實例都是是乾淨完整的。返回一個wrapper對象
})
test('Should have two input & one button', () => {
const inputs = wrapper.findAll('.el-input') // 通過findAll來查找dom或者vue實例
const loginButton = wrapper.find('.el-button') // 通過find查找元素
expect(inputs.length).toBe(2) // 應該有兩個輸入框
expect(loginButton).toBeTruthy() // 應該有一個登錄按鈕。 只要斷言條件不為空或這false,toBeTruthy就能通過。
})

一切看起來很正常。運行測試。結果報錯了。報錯是input.length並不等於2。通過debug斷點查看,確實並沒有找到元素。

這是怎麼回事?哦對,我想起來,形如el-inputel-button其實也相當於是子組件啊,所以shallow並不能將它們渲染出來。在這種情況下,用shallow來渲染就不合適了。所以還是需要用mount來渲染,它會將頁面渲染成它應該有的樣子。

import { mount } from 'vue-test-utils'
import Login from '../../src/components/Login.vue'
let wrapper
beforeEach(() => {
wrapper = mount(Login) // 每次測試前確保我們的測試實例都是是乾淨完整的。返回一個wrapper對象
})
test('Should have two input & one button', () => {
const inputs = wrapper.findAll('.el-input') // 通過findAll來查找dom或者vue實例
const loginButton = wrapper.find('.el-button') // 通過find查找元素
expect(inputs.length).toBe(2) // 應該有兩個輸入框
expect(loginButton).toBeTruthy() // 應該有一個登錄按鈕。 只要斷言條件不為空或這false,toBeTruthy就能通過。
})

測試,還是報錯!還是沒有找到它們。為什麼呢?再想想。應該是我們並沒有將element-ui引入我們的測試裡。因為.el-input實際上是element-ui的一個組件,如果沒有引入它,vue自然無法將一個el-input渲染成<div class="el-input"><input></div>這樣的形式。想通了就好說了,把它引進來。因為我們的項目裡在webpack環境下是有一個main.js作為入口文件的,在測試裡可沒有這個東西。所以Vue自然也不知道你測試裡用到了什麼依賴,需要我們單獨引入:

import Vue from 'vue'
import elementUI from 'element-ui'
import { mount } from 'vue-test-utils'
import Login from '../../src/components/Login.vue'
Vue.use(elementUI)
// ...

再次運行測試,通過!

快照測試

接下來,使用Jest內置的一個特別棒的特性:快照(snapshot)。它能夠將某個狀態下的html結構以一個快照文件的形式存儲下來,以後每次運行快照測試的時候如果發現跟之前的快照測試的結果不一致,測試就無法通過。

當然如果是以後頁面確實需要發生改變,快照需要更新,那麼只需要在執行jest的時候增加一個-u的參數,就能實現快照的更新。

說完了原理來實踐一下。對於登錄頁,實際上我們只需要確保html結構沒問題那麼所有必要的元素自然就存在。因此快照測試寫起來特別方便:

test('Should have the expected html structure', () => {
expect(wrapper.element).toMatchSnapshot() // 調用toMatchSnapshot來比對快照
})

如果是第一次進行快照測試,那麼它會在你的測試文件所在目錄下新建一個__snapshots__的目錄存放快照文件。上面的測試就生成了一個login.spec.js.snap的文件,如下:

// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`Should have the expected html structure 1`] = `
<div
class="el-row content"
>
<div
class="el-col el-col-24 el-col-xs-24 el-col-sm-6 el-col-sm-offset-9"
>
<span
class="title"
>
歡迎登錄
</span>
<div
class="el-row"
>
<div
class="el-input"
>
<!---->
<!---->
<input
autocomplete="off"
class="el-input__inner"
placeholder="賬號"
type="text"
/>
<!---->
<!---->
</div>
<div
class="el-input"
>
<!---->
<!---->
<input
autocomplete="off"
class="el-input__inner"
placeholder="密碼"
type="password"
/>
<!---->
<!---->
</div>
<button
class="el-button el-button--primary"
type="button"
>
<!---->
<!---->
<span>
登錄
</span>
</button>
</div>
</div>
</div>
`;

可以看到它將整個html結構以快照的形式保存下來了。快照測試能確保我們的前端頁面結構的完整性和穩定性。

methods測試

很多時候我們需要測試在某些情況下,Vue中的一些methods能否被觸發。比如本例裡的,我們點擊登錄按鈕應對要觸發loginToDo這個方法。於是就涉及到了methods的測試,這個時候vue-test-utils提供的setMethods這個方法就很有用了。我們可以通過設置(覆蓋)loginToDo這個方法,來查看它是否被觸發了。

注意,一旦setMethods了某個方法,那麼在某個test()內部,這個方法原本的作用將完全被你的新function覆蓋。包括這個Vue實例裡其他methods通過this.xxx()方式調用也一樣。

test('loginToDo should be called after clicking the button', () => {
const stub = jest.fn() // 偽造一個jest的mock funciton
wrapper.setMethods({ loginToDo: stub }) // setMethods將loginToDo這個方法覆寫
wrapper.find('.el-button').trigger('click') // 對button觸發一個click事件
expect(stub).toBeCalled() // 查看loginToDo是否被調用
})

注意到這裡我們用到了jest.fn這個方法,這個在下節會詳細說明。此處你只需要明白這個是jest提供的,可以用來檢測是否被調用的方法。

mock方法測試

接下去就是對登錄這個功能的測試了。由於我們之前把Koa的後端api進行了測試,所以我們在前端測試中,可以默認後端的api接口都是返回正確的結果的。(這也是我們先進行了Koa端測試的原因,保證了後端api的健壯性回到前端測試的時候就能很輕鬆)

雖然道理是說得通的,但是我們如何來默認、或者說“偽造”我們的api請求,以及返回的數據呢?這個時候就需要用上Jest一個非常有用的功能mock了。可以說mock這個詞對很多做前端的朋友來說,不是很陌生。在沒有後端,或者後端功能還未完成的時候,我們可以通過api的mock來實現偽造請求和數據。

Jest的mock也是同理,不過它更厲害的一點是,它能偽造庫。比如我們接下去要用的HTTP請求庫axios。對於我們的頁面來說,登錄只需要發送post請求,判斷返回的success是否是true即可。我們先來mock一下axios以及它的post請求。

jest.mock('axios', () => ({
post: jest.fn(() => Promise.resolve({
data: {
success: false,
info: '用戶不存在!'
}
}))
}))

然後我們可以把axios引入我們的項目了:

import Vue from 'vue'
import elementUI from 'element-ui'
import { mount } from 'vue-test-utils'
import Login from '../../src/components/Login.vue'
import axios from 'axios'
Vue.use(elementUI)
Vue.prototype.$http = axios
jest.mock(....)

等會,你肯定會提出疑問,jest.mock()方法寫在了import axios from 'axios'下面,那麼不就意味著axios是從node_modules裡引入的嗎?其實不是的,jest.mock()會實現函數提升,也就是實際上上面的代碼其實和下面的是一樣的:

jest.mock(....)
import Vue from 'vue'
import elementUI from 'element-ui'
import { mount } from 'vue-test-utils'
import Login from '../../src/components/Login.vue'
import axios from 'axios' // 這裡的axios是來自jest.mock()裡的axios
Vue.use(elementUI)
Vue.prototype.$http = axios

看起來甚至有些var的變量提升的味道。

不過這樣的好處是很明顯的,我們可以在不破壞eslint的規則的情況下采用第一種的寫法而達到一樣的目的。

然後你還會注意到我們用到了jest.fn()的方法,它是jest的mock方法裡很重要的一部分。它本身是一個mock function。通過它能夠實現方法調用的追蹤以及後面會說到的能夠實現創建複雜行為的模擬功能。

繼續我們沒寫完的測試:

test('Failed to login if not typing the correct password', async () => {
wrapper.setData({
account: 'molunerfinn',
password: '1234'
}) // 模擬用戶輸入數據
const result = await wrapper.vm.loginToDo() // 模擬異步請求的效果
expect(result.data.success).toBe(false) // 期望返回的數據裡success是false
expect(result.data.info).toBe('密碼錯誤!')
})

我們通過setData來模擬用戶在兩個input框內輸入了數據。然後通過wrapper.vm.loginToDo()來顯式調用loginTodo的方法。由於我們返回的是一個Promise對象,所以可以用async await將resolve裡的數據拿出來。然後測試是否和預期相符。我們這次是測試了輸入錯誤的情況,測試通過,沒有問題。那如果我接下去要再測試用戶密碼都通過的測試怎麼辦?我們mockaxiospost方法只有一個,難不成還能一個方法輸出多種結果?下一節來詳細說明這個問題。

創建複雜行為測試

回顧一下我們的mock寫法:

jest.mock('axios', () => ({
post: jest.fn(() => Promise.resolve({
data: {
success: false,
info: '用戶不存在!'
}
}))
}))

可以看到,採用這種寫法的話,post請求始終只能返回一種結果。如何做到既能mock這個post方法又能實現多種結果測試?接下去就要用到Jest另一個殺手鐗的方法:mockImplementationOnce。官方的示例如下:

const myMockFn = jest.fn(() => 'default')
.mockImplementationOnce(() => 'first call')
.mockImplementationOnce(() => 'second call');
console.log(myMockFn(), myMockFn(), myMockFn(), myMockFn());
// > 'first call', 'second call', 'default', 'default'

4次調用同一個方法卻能給出不同的運行結果。這正是我們想要的。

於是在我們測試登錄成功這個方法的時候我們需要改寫一下我們對axios的mock方法:

jest.mock('axios', () => ({
post: jest.fn()
.mockImplementationOnce(() => Promise.resolve({
data: {
success: false,
info: '用戶不存在!'
}
}))
.mockImplementationOnce(() => Promise.resolve({
data: {
success: true,
token: 'xxx' // 隨意返回一個token
}
}))
}))

然後開始寫我們的測試:

test('Succeeded to login if typing the correct account & password', async () => {
wrapper.setData({
account: 'molunerfinn',
password: '123'
})
const result = await wrapper.vm.loginToDo()
expect(result.data.success).toBe(true)
})

就在我認為跟之前的測試沒有什麼兩樣的時候,報錯傳來了。先來看看當success為true的時候,loginToDo在做什麼:

if (res.data.success) { // 如果成功
sessionStorage.setItem('demo-token', res.data.token) // 用sessionStorage把token存下來
this.$message({ // 登錄成功,顯示提示語
type: 'success',
message: '登錄成功!'
})
this.$router.push('/todolist') // 進入todolist頁面,登錄成功
}

很快我就看到了錯誤所在:我們的測試環境裡並沒有sessionStorage這個原本應該在瀏覽器端的東西。以及我們並沒有使用vue-router,所以就無法執行this.$router.push()這個方法。

關於前者,很容易找到問題解決辦法

首先安裝一下mock-local-storage這個庫(也包括了sessionStorage)

yarn add mock-local-storage -D
#or
npm install mock-local-storage --save-dev

然後配置一下package.json裡的jest參數:

"jest": {
// ...
"setupTestFrameworkScriptFile": "mock-local-storage"
}

對於後者,閱讀過官方的建議,我們不應該引入vue-router,這樣會破壞我們的單元測試。相應的,我們可以mock它。不過這次是用vue-test-utils自帶的mocks特性了:

const $router = { // 聲明一個$router對象
push: jest.fn()
}
beforeEach(() => {
wrapper = mount(Login, {
mocks: {
$router // 在beforeEach鉤子裡掛載進mount的mocks裡。
}
})
})

通過這個方式,會把$router這個對象掛載到實例的prototype上,就能實現在組件內部通過this.$router.push()的方式來調用了。

上述兩個問題解決之後,我們的測試也順利通過了:

全棧測試實戰:用Jest測試Vue+Koa全棧應用

接下去開始測試Todolist.vue這個組件了。

Todolist.vue的測試

鍵盤事件測試以及隱式事件觸發

類似的我們在test/client目錄下創建一個叫做todolist.spec.js的文件。

先把上例中的一些環境先預置進來:

import Vue from 'vue'
import elementUI from 'element-ui'
import { mount } from 'vue-test-utils'
import Todolist from '../../src/components/Todolist.vue'
import axios from 'axios'
Vue.use(elementUI)
jest.mock(...) // 後續補充
Vue.prototype.$http = axios
let wrapper
beforeEach(() => {
wrapper = mount(Todolist)
wrapper.setData({
name: 'Molunerfinn', // 預置數據
id: 2
})
})

先來個簡單的,測試數據是否正確:

// test 1
test('Should get the right username & id', () => {
expect(wrapper.vm.name).toBe('Molunerfinn')
expect(wrapper.vm.id).toBe(2)
})

不過需要注意的是,todolist這個頁面在created階段就會觸發getUserInfogetTodolist這兩個方法,而我們的wrapper是相當於在mounted階段之後的。所以在我們拿到wrapper的時候,createdmounted等生命週期的鉤子其實已經運行了。本例裡getUserInfo是從sessionStorage裡取值,不涉及ajax請求。但是getTodolist涉及請求,因此需要在jest.mock方法裡為其配置一下,否則將會報錯:

jest.mock('axios', () => ({
get: jest.fn()
// for test 1
.mockImplementationOnce(() => Promise.resolve({
status: 200,
data: {
result: []
}
}))
}))

上面說到的getTodolistgetUserInfo就是在測試中需要注意的隱式事件,它們並不受你測試的控制就在組件裡觸發了。

接下來開始進行鍵盤事件測試。其實跟鼠標事件類似,鍵盤事件的觸發也是以事件名來命名的。不過對於一些常見的事件,vue-test-utils裡給出了一些別名比如:

enter, tab, delete, esc, space, up, down, left, right。你在書寫測試的時候可以直接這樣:

const input = wrapper.find('.el-input')
input.trigger('keyup.enter')

當然如果你需要指定某個鍵也是可以的,只需要提供keyCode就行:

const input = wrapper.find('.el-input')
input.trigger('keyup', {
which: 13 // enter的keyCode為13
})

於是我們把這個測試完善一下,這個測試是測試當我在輸入框激活的情況下按下回車鍵能否觸發addTodos這個事件:

test('Should trigger addTodos when typing the enter key', () => {
const stub = jest.fn()
wrapper.setMethods({
addTodos: stub
})
const input = wrapper.find('.el-input')
input.trigger('keyup.enter')
expect(stub).toBeCalled()
})

沒有問題,一次通過。

注意到我們在實際開發時,在組件上調用原生事件是需要加.native修飾符的:

<el-input placeholder="請輸入待辦事項" v-model="todos" @keyup.enter.native="addTodos"></el-input>

但是在vue-test-utils裡你是可以直接通過原生的keyup.enger來觸發的。

wrapper.update()的使用

很多時候我們要跟異步打交道。尤其是異步取值,異步賦值,頁面異步更新。而對於使用Vue來做的實際開發來說,異步的情況簡直太多了。

還記得nextTick麼?很多時候,我們要獲取一個變更的數據結果,不能直接通過this.xxx獲取,相應的我們需要在this.$nextTick()裡獲取。在測試裡我們也會遇到很多需要異步獲取的情況,但是我們不需要nextTick這個辦法,相應的我們可以通過async await配合wrapper.update()來實現組件更新。例如下面這個測試添加todo成功的例子:

test('Should add a todo if handle in the right way', async () => {
wrapper.setData({
todos: 'Test',
stauts: '0',
id: 1
})
await wrapper.vm.addTodos()
await wrapper.update()
expect(wrapper.vm.list).toEqual([
{
status: '0',
content: 'Test',
id: 1
}
])
})

在本例中,從進頁面到添加一個todo並顯示出來需要如下步驟:

  1. getUserInfo -> getTodolist
  2. 輸入todo並敲擊回車
  3. addTodos -> getTodolist
  4. 顯示添加的todo

可以看到總共有3個ajax請求。其中第一步不在我們test()的範圍內,2、3、4都是我們能控制的。而addTodos和getTodolist這兩個ajax請求帶來的就是異步的操作。雖然我們mock方法,但是本質上是返回了Promise對象。所以還是需要用await來等待。

注意你在jest.mock()裡要加上相應的mockImplementationOnce的get和post請求。

所以第一步await wrapper.vm.addTodos()就是等待addTodos()的返回。
第二步await wrapper.update()實際是在等待getTodolist的返回。

缺一不可。兩步等待之後我們就可以通過斷言數據list的方式測試我們是否拿到了返回的todo的信息。

接下去的就是對todo的一些增刪改查的操作,採用的測試方法已經和前文所述相差無幾,不再贅述。至此所有的獨立測試用例的說明就說完了。看看這測試通過的成就感:

全棧測試實戰:用Jest測試Vue+Koa全棧應用

不過在測試中我還有關於調試的一些經驗想分享一下,配合調試能更好的判斷我們的測試的時候發生的不可預知的問題所在。

用VSCode來調試測試

由於我自己是使用VSCode來做的開發和調試,所以一些用其他IDE或者編輯器的朋友們可能會有所失望。不過沒關係,可以考慮加入VSCode陣營嘛!

本文撰寫的時候採用的nodejs版本為8.9.0,VSCode版本為1.18.0,所以所有的debug測試的配置僅保證適用於目前的環境。其他環境的可能需要自行測試一下,不再多說。

關於jest的調試的配置如下:(注意配置路徑為VScode關於本項目的.vscode/launch.json

{
// Use IntelliSense to learn about possible Node.js debug attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"name": "Debug Jest",
"type": "node",
"request": "launch",
"program": "${workspaceRoot}/node_modules/jest-cli/bin/jest.js",
"stopOnEntry": false,
"args": [
"--runInBand",
"--forceExit"
],
"cwd": "${workspaceRoot}",
"preLaunchTask": null,
"runtimeExecutable": null,
"runtimeArgs": [
"--nolazy"
],
"env": {
"NODE_ENV": "test"
},
"console": "integratedTerminal",
"sourceMaps": true
}
]
}

配置完上面的配置之後,你可以在DEBUG面板裡(不要跟我說你不知道什麼是DEBUG面板~)找到名為Debug Jest的選項:

全棧測試實戰:用Jest測試Vue+Koa全棧應用

然後你可以在你的測試文件裡打斷點了:

全棧測試實戰:用Jest測試Vue+Koa全棧應用

然後運行debug模式,按那個綠色啟動按鈕,就能進入DEBUG模式,當運行到斷點處就會停下:

全棧測試實戰:用Jest測試Vue+Koa全棧應用

於是你可以在左側面板的LocalClosure裡找到當前作用域下你所需要的變量值、變量類型等等。充分運用VSCode的debug模式,開發的時候查錯和調試的效率都會大大加大。

總結

本文用了很大的篇幅描述瞭如何搭建一個Jest測試環境,並在測試過程中不斷完善我們的測試環境。講述了Koa後端測試的方法和測試覆蓋率的提高,講述了Vue前端單元測試環境的搭建以及許多相應的測試實例,以及在測試過程中不停地遇到問題並解決問題。能夠看到此處的都不是一般有耐心的人,為你們鼓掌~也希望你們通過這篇文章能過對本文在開頭提出的幾個重點在心中有所體會和感悟:

可以瞭解到測試的意義,Jest測試框架的搭建,前後端測試的異同點,如何寫測試用例,如何查看測試結果並提升我們的測試覆蓋率,100%測試覆蓋率是否是必須,以及在搭建測試環境、以及測試本身過程中遇到的各種疑難雜症。

本文所有的測試用例以及整體項目實例你都可以在我的vue-koa-demo的github項目中找到源代碼。如果你喜歡我的文章以及項目,歡迎點個star~如果你對我的文章和項目有任何建議或者意見,歡迎在文末評論或者在本項目的issues跟我探討!

本文首發於我的博客,歡迎踩點~

參考鏈接

Koa相關

Supertest搭配koa報錯

測試完自動退出

Async testing Koa with Jest

How to use Jest to test Express middleware or a funciton which consumes a callback?

A clear and concise introduction to testing Koa with Jest and Supertest

Debug jest with vscode

Test port question
Coverage bug

Eaddrinuse bug

Istanbul ignore

Vue相關

vue-test-utils

Test Methods and Mock Dependencies in Vue.js with Jest

Storage problem

相關文章

Electronvue開發實戰0——Electronvue入門

Vue組件開發實錄:組件的三種調用方式

我的2017年前端之路總結

我用Electron做了一個圖片上傳的工具——PicGo