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

本文首發於個人博客,歡迎踩點~html

前言

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

簡介

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

項目結構

有了以前的項目結構做爲骨架,加入Jest測試框架就很簡單了。node

.
├── 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文件複製代碼

能夠看到新增的或者說更新的東西只有幾個:mysql

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

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

測試用到的一些關鍵依賴

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

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

剩下依賴能夠項目demo倉庫ios

搭建Jest測試環境

對於測試來講,我也是個新手。至於爲何選擇了Jest,而不是其餘框架(例如mocha+chai、jasmine等),我以爲有以下我本身的觀點(固然你也能夠不採用它):git

  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複製代碼

很簡單對吧。github

配置

因爲我項目的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測試

重現一下以前的應用的操做流程,能夠發現應用分爲登陸前和登陸後兩種狀態。

能夠根據操做流程或者後端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" // 會在命令行界面輸出簡單的測試報告
  ]
}複製代碼

而後咱們再進行一遍測試,能夠看到在終端裏已經輸出了簡易的測試報告總結:

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

首頁是個概覽,跟命令行裏輸出的內容差很少。不過咱們能夠往深了看,能夠點擊左側的File提供的目錄:

而後咱們能夠看到沒有被覆蓋到代碼行數(50)以及有一個函數沒有被測試到:

一般咱們沒有測試到的函數也伴隨着代碼行數沒有被測試到。咱們能夠看到在本例裏,app的error事件沒有被觸發過。想一想也是的,咱們的測試都是創建在合法的api請求的基礎上的。因此天然不會觸發error事件。所以咱們須要寫一個測試用例來測試這個.on('error')的函數。

一般這樣的測試用例並非特別好寫。不過好在咱們能夠嘗試去觸發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%了。

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

能夠看到是個三元表達式並無測試完整。(三元表達式也算分支)咱們測試了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)
})複製代碼

再次跑測試:

哈,成功作到了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"
  ]
}複製代碼

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

這樣咱們就完成了後端的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()的方式來調用了。

上述兩個問題解決以後,咱們的測試也順利經過了:

接下去開始測試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()的範圍內,二、三、4都是咱們能控制的。而addTodos和getTodolist這兩個ajax請求帶來的就是異步的操做。雖然咱們mock方法,可是本質上是返回了Promise對象。因此仍是須要用await來等待。

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

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

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

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

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

用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的選項:

而後你能夠在你的測試文件裏打斷點了:

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

因而你能夠在左側面板的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

相關文章
相關標籤/搜索