前端測試之 Jest 單元測試

1、Jest 簡介

  1. 優點: 速度快、API簡單、配置簡單
  2. 前置: Jest 不支持 ES Module 語法,須要安裝 babel
npm install -D @babel/core @babel/preset-env
複製代碼

.babelrcjavascript

{
  "presets": [
    [
      "@babel/preset-env", {
        "targets": {
          "node": "current"
        }
      }
    ]
  ]
}
複製代碼

jest 在運行前會檢查是否安裝 babel,若是安裝了會去取 .babelrc 文件,結合 babel 將代碼進行轉化,運行轉化後的代碼。 3. jest 默認配置css

npx jest --init
複製代碼
  1. jest 模式
  • jest --watchAll:當發現測試文件變更,將全部測試文件從新跑一遍
  • jest --watch:須要和 git 結合使用,會比較現有文件和 commit 的文件的差別,只測試差別文件

2、Jest 匹配器

常見匹配器

  • toBe
  • toEqual:判斷對象內容是否相等
  • toMatchObject:expect(obj).toMatchObject(o),指望 o 中包含 obj
  • toBeNull
  • toBeUndefined
  • toBeDefinded
  • toBeTruthy
  • toBeFalsy
  • not:用於否認,好比 .not.toBeTruthy()

Number 相關

  • toBeGreaterThan(大於) / toBeGreaterThanOrEqual(大於等於)
  • toBeCloseTo:用於比較浮點數,近似相等時斷言成立
  • toBeLessThan / toBeLessThanOrEqual

String 相關

  • toMatch:參數能夠傳字符串或正則

Array Set 相關

  • toContain

異常匹配器

  • toThrow:
const throwError = () => {
  throw new Error('error')
}

it('can throw error', () => {
  expect(throwError).toThrow('error') // 判斷throw函數能夠拋出異常,異常信息爲 "error"。也能夠寫正則
})
複製代碼

這裏有個小技巧:當咱們想忽略掉單個文件中的其餘測試用例,只針對一個測試用例作調試的時候,能夠加上 .onlyhtml

it.only('test', () => {
  // ...
})
複製代碼

但這並不會忽略其餘測試文件的測試用例前端

3、測試異步代碼

這裏有三個異步方法,對這三個方法進行代碼測試,"www.dell-lee.com/react/api/d…" 會返回 {success: true}, "www.dell-lee.com/react/api/4…" 則不存在。vue

import axios from 'axios'

export function getData1() {
  return axios.get('http://www.dell-lee.com/react/api/demo.json')
}

export function getData2(fn) {
  axios.get('http://www.dell-lee.com/react/api/demo.json').then(res => {
    fn(res)
  })
}

export function get404() {
  return axios.get('http://www.dell-lee.com/react/api/404.json')
}
複製代碼

對於異步代碼測試,時機很重要,必須保證咱們的測試用例在異步代碼走完以後才結束。有如下幾種辦法:java

  1. done,控制測試用例結束的時機
  2. 若是函數執行的返回值是 Promise,將這個 Promise return 出去
  3. async + await
import {getData1, getData2, get404} from './fetchData/fetchData'

it('getData1 方法1', (done) => {
  getData1().then(res => {
    expect(res.data).toEqual({
      success: true
    })
    done()  // 若是不加 done,還沒執行到 .then 方法,測試用例已經結束了
  })
})

it('getData1 方法2', () => {
  return getData1().then(res => {
    expect(res.data).toEqual({
      success: true
    })
  })
})

it('getData2 方法2', (done) => {
  getData2((res) => {
    expect(res.data).toEqual({
      success: true
    })
    done()
  })
})

it('getData1 方法3', async () => {
  const res = await getData1()
  expect(res.data).toEqual({
    success: true
  })
})

/*********** 重點關注 ***********/
it('get404', (done) => {
  expect.assertions(1)
  get404().catch(r => {
    expect(r.toString()).toMatch('404')
    done()
  })
})
複製代碼

重點講一下上面的最後一個測試用例,假設咱們如今有一個返回的是 404 的接口,咱們須要對這個接口測試,指望他返回 404。 咱們用 catch 捕獲,在 catch 中判斷。node

可是,假如這個接口返回的不是 404,而是正常返回 200,這個 catch 則不會執行,expect 也不會執行,測試依然是經過的。這不符合咱們的預期!因此,咱們須要加上 expect.assertions(1) 進行斷言:下面必定會執行一個 expectreact

固然,也能夠用 async await 方法進行 404 接口的測試ios

it('get404 方法3', async () => {
  await expect(get404()).rejects.toThrow()
})
複製代碼

4、Jest 中的一些鉤子函數

  • beforeAll:全部用例開始執行前
  • beforeEach:每一個用例執行前
  • afterEach
  • afterAll
  • describe

前四個鉤子使用起來很簡單,調用方法以下:git

beforeAll(() => {
  // ...
})
複製代碼

若是測試先後要作一些處理,儘量寫在這些鉤子函數中,他能保證必定的執行順序。

describe 能夠用來進行用例分組,爲了讓咱們的測試輸出結果更好看,更有層次。 同時,在每一個 describe 中都有上面 4 個鉤子函數的存在,咱們來看看具體的狀況:

describe('測試 Button 組件', () => {
  beforeAll(...)  // 1
  beforeEach(...) // 2
  afterEach(...)  // 3
  afterAll(...)   // 4

  describe('測試 Button 組件的事件', () => {
    beforeAll(...)  // 5
    beforeEach(...) // 6
    afterEach(...)  // 7
    afterAll(...)   // 8
    it('event1', ()=>{...})
  })
})
複製代碼

上面鉤子函數的執行順序是: 1 > 5 > 2 > 6 > 3 > 7 > 4 > 8
外部的鉤子函數對 describe 內部的用例也生效,執行順序爲:先外部後內部

5、Jest 中的 mock

1. 在 Jest 中 mock 異步方法

前面提到了能夠測試異步代碼,對於一些接口都能進行請求測試。但假如每個接口都真的發起請求,那一次測試須要耗費的時間是不少的。 這時候咱們能夠模擬請求方法,步驟以下:

  1. mock.js 中導出了咱們的請求方法
import axios from 'axios'

export function getData() {
  return axios.get('http://www.dell-lee.com/react/api/demo.json')
}
複製代碼
  1. 在 mock.js 的同級目錄下建一個 mocks 的文件夾,文件夾內創建對應文件名的文件,這個文件就是導出的方法就是模擬請求的方法
    1580732355(1)
    這裏咱們直接返回一個 Promise,把假數據 resolve 出去
export function getData() {
  return Promise.resolve({
    success: true
  })
}
複製代碼
  1. 測試用例部分: 這裏有一個須要注意的jest.mock 不能寫在任何鉤子函數裏,由於鉤子函數的執行時機問題,beforeAll 也不行,當鉤子函數執行時,沒有寫在鉤子函數裏面的代碼已經執行了,也就是已經 import 了!
jest.mock('./mock/mock.js')  // 聲明下面引入的 getData 方法是 jest 模擬的,若是不須要引入該方法則不須要聲明

import {getData} from './mock/mock.js'  // 導入 mock.js,但實際上 jest 會導入 __mocks__ 下的 mock.js

test('mock 方法測試', () => {
  getData().then(data => {
    expect(data).toEqual({
      success: true
    })
    done()
  })
  
})
複製代碼

除了上面的這種辦法,還能在 jest.config.js 中配置自動開啓 mock,這樣 jest 會自動去查找當前文件同級有沒有 mock 文件夾,裏面有沒有對應文件

module.exports = {
  automock: true
}
複製代碼

講了兩種 mock 的方法,還有一種極端狀況須要避免 mock:
咱們在 mock.js 中定義了一個須要 mock 的 getData 方法,又另外定義了一個不須要 mock 的普通方法,當咱們在測試文件導入的時候,須要避免 jest 去 mocks/mock.js 下找這個普通方法,這裏須要用 jest 提供的方法導入:

const { regularMethod } = jest.requireActual('./mock/mock.js')
複製代碼

2. 用 Jest 操控時間

當咱們有以下代碼須要測試的時候:

export default (fn) => {
  setTimeout(() => {
    fn()
  }, 3000)
}
複製代碼

咱們不可能老是去等待定時器,這時候咱們要用 Jest 來操做時間!步驟以下:

  1. 經過 jest.useFakeTimers() 使用 jest 「自制的」 定時器,這裏放在 beforeEach 裏面是由於快進時間可能被調用屢次,我但願在每一個測試用例裏,這個時鐘都是初始狀態,不會互相影響。
  2. 執行 timer 函數以後,快進時間 3 秒 jest.advanceTimersByTime(3000),這個方法能夠調用任意次,快進的時間會疊加。
  3. 這時候咱們已經穿梭到了 3 秒後,expect 也能生效了!

特別說明一下:jest.fn() 生成的是一個函數,這個函數能被監聽調用過幾回

import timer from './timer/timer'

beforeEach(() => {
  jest.useFakeTimers()
})

it('timer 測試', () => {
  const fn = jest.fn()
  timer(fn)
  jest.advanceTimersByTime(3000)
  expect(fn).toHaveBeenCalledTimes(1)
})
複製代碼

3. mock 類

一樣的,當咱們只關注類的方法是否被調用,而不關心方法調用產生的結果時,能夠 mock 類

在 util/util.js 中定義了 Util 類

export class Util {
  a() {}
  b() {}
}
複製代碼

在 util/useUtil 中調用了這個類

import {Util} from './util'

export function useUtil() {
  let u = new Util()
  u.a()
  u.b()
}
複製代碼

咱們須要測試 u.a 和 u.b 被調用,jest.mock('./util/util') 會將 Util、Util.a、Util.b 都 mock 成 jest.fn

測試用例以下:

jest.mock('./util/util')  // mock Util 類
import {Util} from './util/util'
import {useUtil} from './util/uesUtil'

test('util 的實例方法被執行了', () => {
  useUtil()
  expect(Util).toHaveBeenCalled()
  expect(Util.mock.instances[0].a).toHaveBeenCalled()
  expect(Util.mock.instances[0].b).toHaveBeenCalled()
})
複製代碼

6、結合 Vue組件 進行單元測試

1. 簡單用例入門

Vue 提供了 @vue/test-utils 來幫助咱們進行單元測試,建立 Vue 項目的時候勾選測試選項會自動幫咱們安裝。

先來介紹兩個經常使用的掛載方法:

  • mount:會將組件以及組件包含的子組件都進行掛載
  • shallowMount:淺掛載,只會掛載組件,忽略子組件

再來看一個簡單的測試用例:

import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'

describe('HelloWorld.vue', () => {
  it('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    })
    expect(wrapper.props('msg')).toBe(msg)
  })
})
複製代碼

shallowMount 會返回一個 wrapper,這個 wrapper 上面會包含不少幫助咱們測試的方法,詳見

2. 快照測試

快照測試的意思是,會將組件像拍照同樣拍下來,存底。下次運行測試用例的時候,若是組件發生變化,和快照不同了,就會報錯。

測試用例寫法以下: 第一次測試會保存 wrapper 的快照,第二次會比較當前 wrapper 和快照的區別

describe('HelloWorld.vue', () => {
  it('renders props.msg when passed', () => {
    const msg = 'new message'
    const wrapper = shallowMount(HelloWorld, {
      propsData: { msg }
    })
    expect(wrapper).toMatchSnapshot()
  })
})
複製代碼

咱們再來看看快照長什麼樣子:

1580787793(1)
能夠看到,快照實際保存的就是組件渲染以後的 html 部分,css 部分沒有保存,在元素上綁定的 @click 等一些事件也不會保存, 因此快照適合進行 DOM 節點是否變化的測試。

當快照發生變化時,咱們能夠在終端按 u 進行更新快照

1580788050(1)

3. 覆蓋率測試

覆蓋率測試是對測試徹底程度的一個評估,測試覆蓋到的業務代碼越多,覆蓋率越高。

在 jest.config.js 中咱們能夠設置 collectCoverageFrom,來設置須要進行覆蓋率測試的文件,這裏咱們測試一下全部的 .vue 文件,忽略 node_modules 下全部文件。

要注意,在 Vue 中配置 jest,參考文檔

而後添加一條 script 命令,就能進行測試了:

"test:unit": "vue-cli-service test:unit --coverage"
複製代碼

執行命令會生成 coverage 文件夾,Icov-report/index.html 裏會可視化展現咱們的測試覆蓋率

4. 結合 Vuex 進行測試

若是咱們在組件中引入了 Vuex 狀態或者使用了相關方法,在測試用例裏,掛載組件的時候只須要傳入 vuex 的 store 便可。

import store from '@/store/index'

const wrapper = mount(HelloWorld, {
    store
})
複製代碼

7、寫在最後

1. 單元測試 or 集成測試?

就拿 shallowMount 來講,這個 api 就很適合單元測試,單元測試不關注單元之間的聯繫,對每一個單元進行獨立測試, 這也使得它代碼量大,測試間過於獨立。在進行一些函數庫的測試,各個函數比較獨立的時候,就很適合單元測試。
在進行一些業務組件測試時,須要關注組件間的聯繫,比較適合用集成測試。

2. TDD or BDD?

TDD:測試驅動開發,先寫測試用例,而後根據用例寫代碼,比較關注代碼自己。以下:

describe('input 輸入回車,向外觸發事件,data 中的 inputValue 被賦值', () => {
  const wrapper = shallowMount(TodoList)
  const inputEle = wrapper.find('input').at(0)
  const inputContent = '用戶輸入內容'
  inputEle.setValue(inputContent)
  // expect:add 事件被 emit
  except(wrapper.emitted().add).toBeTruthy()
  // expect:data 中的 inputValue 被賦值爲 inputContent
  except(wrapper.vm.inputValue).toBe(inputContent)
})
複製代碼

TDD 關注代碼內部如何實現,關注事件是否觸發?屬性是否設置?data 數據是否被更新?

BDD:用戶行爲驅動開發,先寫完業務代碼,而後站在用戶的角度去測試功能,不關注代碼實現過程,只是經過模擬用戶操做測試功能
好比下面這個用例:

describe('TodoList 測試', () => {
  it(` 1. 用戶在 header 輸入框輸入內容 2. 鍵盤迴車 3. 列表項增長一項,內容爲用戶輸入內容 `, () => {
    // 掛載 TodoList 組件
    const wrapper = mount(TodoList)
    // 模擬用戶輸入
    const inputEle = wrapper.find('input').at(0)
    const inputContent = '用戶輸入內容'
    inputEle.setValue(inputContent)
    // 模擬觸發的事件
    inputEle.trigger('content')
    inputEle.trigger('keyup.enter')
    // expect:列表項增長對應內容
    const listItems = wrapper.find('.list-item')
    expect(listItems.length).toBe(1)  // 增長 1 項
    expect(listItems.at(0).text()).toContain(inputContent)  // 增長 1 項
  })
})
複製代碼

參考:

前端要學的測試課

相關文章
相關標籤/搜索