前端測試之Jest深刻淺出

1. 爲何要作前端測試

首先,我認爲前端測試並非全部項目都必須的,由於寫測試代碼是須要要花費必定時間的,當項目比較簡單的時候,花時間寫測試代碼可能反而會影響開發效率,可是須要指出的是,咱們前端開發過程當中,編寫測試代碼,有如下這些好處:javascript

  1. 更快的發現bug,讓絕大多數bug在開發階段發現解決,提升產品質量
  2. 比起寫註釋,單元測試多是更好的選擇,經過運行測試代碼,觀察輸入和輸出,有時會比註釋更能讓別人理解你的代碼(固然,重要的註釋仍是要寫的。。。)
  3. 有利於重構,若是一個項目的測試代碼寫的比較完善,重構過程當中改動時能夠迅速的經過測試代碼是否經過來檢查重構是否正確,大大提升重構效率
  4. 編寫測試代碼的過程,每每可讓咱們深刻思考業務流程,讓咱們的代碼寫的更完善和規範。

2. 什麼是TDDBDD

2.1 TDD與單元測試

2.1.1 什麼是TDD

所謂TDD(Test Driven Development),即測試驅動開發,簡單的來講就是先編寫測試代碼,而後以使得全部測試代碼都經過爲目的,編寫邏輯代碼,是一種以測試來驅動開發過程的開發模式。html

2.1.2 單元測試

所謂單元測試(unit testing),是指對軟件中的最小可測試單元進行檢查和驗證。通俗的講,在前端,單元能夠理解爲一個獨立的模塊文件,單元測試就是對這樣一個模塊文件的測試。前端

對於一個獨立的模塊(ES6模塊),由於功能相對獨立,因此咱們能夠首先編寫測試代碼,而後根據測試代碼指導編寫邏輯代碼。vue

因此提到TDD,這裏的測試通常是指單元測試java

2.2 BDD與集成測試

2.2.1 什麼是BDD

所謂BDD(Behavior Driven Development),即行爲驅動開發,簡單的來講就是先編寫業務邏輯代碼,而後以使得全部業務邏輯按照預期結果執行爲目的,編寫測試代碼,是一種以用戶行爲來驅動開發過程的開發模式。node

2.2.2 集成測試

所謂集成測試(Integration Testing),是指對軟件中的全部模塊按照設計要求進行組裝爲完整系統後,進行檢查和驗證。通俗的講,在前端,集成測試能夠理解爲對多個模塊實現的一個交互完整的交互流程進行測試。react

對於多個模塊(ES6模塊)組成的系統,須要首先將交互行爲完善,才能按照預期行爲編寫測試代碼。webpack

因此提到BDD,這裏的測試通常是指集成測試。ios

3. Jest使用---引言部分

3.1 咱們如何寫測試代碼?

若是咱們以前歷來沒有接觸過測試代碼,那讓咱們本身來設計測試代碼的寫法,會是什麼樣呢?咱們須要讓測試代碼簡單,通俗易懂,好比咱們舉個例子以下:git

export function findMax (arr) {
    return Math.max(...arr)
}
複製代碼

咱們寫了一個很簡單的獲取數組最大值的函數(你可能以爲這樣寫並不嚴謹,但咱們爲了簡單,暫時假設輸入是非空數值數組),若是對這個函數寫一個測試其正確與否的測試程序,它可能構思是這樣的:

我指望 findMax([1, 2, 4, 3]) 的結果是 4
複製代碼

進一步轉化爲英文:

I expect findMax([1, 2, 4, 3]) to be 4
複製代碼

用程序性的語言表示,expect做爲一個函數,爲它傳入想要測試的對象(findMax函數),把輸出結果也作一層封裝toBe(4):

expect(findMax([1, 2, 4, 3])).toBe(4)  // 有內味了
複製代碼

更進一步,咱們想要增長一些描述性信息,好比

測試findMax函數,我指望 findMax([1, 2, 4, 3]) 的結果是 4
複製代碼

這個時候,咱們能夠再作一層封裝,定義一個test函數,它有兩個參數,第一個參數是一些描述性信息(這裏是 測試findMax函數),第二個參數是一個函數,函數裏能夠執行咱們上面的邏輯,以下:

test('findMax函數輸出', () => {
    expect(findMax([1, 2, 4, 3])).toBe(4) // 內味更深了
})
複製代碼

3.2 簡單的本身實現測試代碼

咱們本身能夠簡單的實現下test函數和expect函數,由於存在鏈式調用toBe,因此expect函數最終應該返回一個具備toBe方法的對象,以下:

// expect函數
function expect (value) {
    return {
        toBe: (toBeValue) => {
            if (toBeValue === value) {
                console.log('測試經過!')
            } else {
                throw new Error('測試不經過!')
            }
        }
    }
}

// test函數
function test (msg, func) {
    try {
        func()
        console.log(`${msg}測試過程無異常!`)
    } catch (err) {
        console.error(`${msg}測試過程出錯!`)
    }
}
複製代碼

咱們的測試方法,只是對數字作了簡單的測試,實際項目中,須要測試的類型是不少的,這個時候咱們就能夠選擇一些比較成熟的測試框架。一個簡單好用,功能強大的工具就呈如今咱們面前,它就是jest

4. Jest使用---入門部分

4.1 準備工做

咱們這部分的例子主要是爲了介紹jest最基本的用法,首先咱們先簡單的搭建一下演示環境。

第一步,使用npm init -y(個人node版本是v12.14.1npm版本是v6.13.4)初始化項目

第二步,安裝jest npm install --save-dev jest(安裝能夠參考官網

第三步,運行npx jest --init命令,生成一份jest的配置文件jest.config.js,個人選擇以下

第四步,運行npm i babel-jest @babel/core @babel/preset-env -D安裝babel,而且配置.babelrc以下

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

第五步,根目錄下創建src文件夾,新建兩個文件basic.jsbasic.test.js

第六步,package.json增長一條命令:

"scripts": {
    "test": "jest"
  },
複製代碼

以上六步完成後,咱們的項目結構應該以下圖

4.2 最基本的jest用法

接下來咱們採用TDD加單元測試的方式來學習jest基本用法:

首先,在basic.js裏定義兩個工具函數

// 1. 尋找最大值
export function findMax (arr) {
    
}

// 2. 給定一個整數數組 nums 和一個目標值 target,在該數組中找出和爲目標值的那 兩個 整數,若是存在,返回true,不然返回false
export function twoSum (nums, target) {

};
複製代碼

既然是TDD,咱們首先編寫測試代碼,在這個過程當中,咱們逐步學習各類jest的基本用法。測試代碼在basic.test.js文件中編寫:

import { findMax, twoSum } from './basic'

// 指望findMax([2, 6, 3])執行後結果爲6
test('findMax([2, 6, 3])', () => {
    expect(findMax([2, 6, 3])).toBe(6)
})

// 指望twoSum([2, 3, 4, 6], 10)執行後結果爲true
test('twoSum([2, 3, 4, 6], 10)', () => {
    expect(twoSum([2, 3, 4, 6], 10)).toBe(true)
})
複製代碼

從上面代碼,咱們能夠看到,jest測試代碼的寫法,和以前咱們本身寫的是同樣的(固然啦,原本就是模仿jest的),此時咱們運行npm test命令,觀察命令行輸出以下:

注意我紅框裏的部分, Expected表明指望函數執行的結果,也就是 toBe裏的那個值, Received表明實際執行函數獲得的結果,由於咱們尚未編寫業務代碼,因此 Received都是 undefined,最後顯示一共 1個測試文件( Test Suites)和 2條測試代碼,它們都測試失敗了。

接下來咱們完善basic.js裏的邏輯

// 1. 尋找最大值
export function findMax (arr) {
    return Math.max(...arr)
}

// 2. 給定一個整數數組 nums 和一個目標值 target,在該數組中找出和爲目標值的那 兩個 整數,若是存在,返回true,不然返回false
export function twoSum (nums, target) {
    for (let i = 0; i < nums.length - 1; i++) {
       for (let j = i + 1; j < nums.length; j++) {
           if (nums[i] + nums[j] === target) {
               return true
           }
       } 
    }
    return false
};
複製代碼

而後咱們再次運行npm test,獲得結果以下

咱們能夠看到,全部測試用例都經過了(直觀的就是都綠了)。這種首先全部測試用例都沒有經過(一片紅),隨着咱們開發過程的進行,一步步的,最終測試代碼都經過(一片綠)的過程,就是 TDD和單元測試的開發過程。

4.3 更多的jest matchers

像是上小節,在expect函數後面跟着的判斷結果的toBejest中被稱爲matcher,咱們這一小節就來介紹另一些經常使用的matchers

4.3.1 toEqual

咱們首先改造下剛剛的twoSum函數,讓它返回找到的兩個數的索引數組(leetcode第一題)

// 2. 給定一個整數數組 nums 和一個目標值 target,在該數組中找出和爲目標值的那 兩個 整數,
// 並返回他們的數組下標(假設每種輸入只會對應一個答案,數組中同一個元素不能使用兩遍)。
export function twoSum (nums, target) {
    for (let i = 0; i < nums.length - 1; i++) {
       for (let j = i + 1; j < nums.length; j++) {
           if (nums[i] + nums[j] === target) {
               return [i, j]
           }
       } 
    }
    return []
};
複製代碼

接下來測試代碼部分咱們只保留對twoSum函數的測試,並同步修改測試代碼

test('twoSum([2, 3, 4, 6], 10)', () => {
    expect(twoSum([2, 3, 4, 6], 10)).toBe([2, 3])
})
複製代碼

咱們的指望是函數執行的結果是[2, 3]這樣的數組,看起來沒問題,運行npm test

咱們發現並無經過測試,這是由於,toBe能夠判斷基本類型數據,可是對於數組,對象這樣的引用類型是沒辦法判斷的,這個時候,咱們就須要使用toEqual

test('twoSum([2, 3, 4, 6], 10)', () => {
    expect(twoSum([2, 3, 4, 6], 10)).toEqual([2, 3])
})
複製代碼

改爲toEqual以後,測試代碼就成功了

4.3.2 判斷邏輯真假相關的一些matchers

這部份內容很簡單,也比較多,因此直接在代碼裏註釋說明:

test('變量a是否爲null', () => {
    const a = null
    expect(a).toBeNull()
})

test('變量a是否爲undefined', () => {
    const a = undefined
    expect(a).toBeUndefined()
})

test('變量a是否爲defined', () => {
    const a = null
    expect(a).toBeDefined()
})

test('變量a是否爲true', () => {
    const a = 1
    expect(a).toBeTruthy()
})

test('變量a是否爲false', () => {
    const a = 0
    expect(a).toBeFalsy()
})
複製代碼

測試結果以下:

4.3.3 not修飾符

很簡單,not就是對matcher的否認

test('test not', () => {
    const temp = 10
    expect(temp).not.toBe(11)
    expect(temp).not.toBeFalsy()
    expect(temp).toBeTruthy()
})
複製代碼

測試結果以下:

4.3.4 判斷數字相關的一些matchers

這部份內容很簡單,也比較多,因此直接在代碼裏註釋說明:

// 判斷數num是否大於某個數
test('toBeGreaterThan', () => {
    const num = 10
    expect(num).toBeGreaterThan(7)
})

// 判斷數num是否大於等於某個數
test('toBeGreaterThanOrEqual', () => {
    const num = 10
    expect(num).toBeGreaterThanOrEqual(10)
})

// 判斷數num是否小於某個數
test('toBeLessThan', () => {
    const num = 10
    expect(num).toBeLessThan(20)
})

// 判斷數num是否小於等於某個數
test('toBeLessThanOrEqual', () => {
    const num = 10
    expect(num).toBeLessThanOrEqual(10)
    expect(num).toBeLessThanOrEqual(20)
})
複製代碼

測試結果以下:

上面介紹的都是整數判斷,十分簡單,可是若是是浮點數相關的判斷,會不太同樣,好比,咱們知道0.1 + 0.2 = 0.3這個式子在數學中沒有問題,可是在計算機中,因爲精度問題,這個0.1 + 0.2結果若是用toBe結果並非準確的0.3,若是咱們想要判斷浮點數的相等,在jest中提供了一個toBeCloseTomatcher能夠解決:

test('toBe', () => {
    const sum = 0.1 + 0.2
    expect(sum).toBe(0.3)
})

test('toBeCloseTo', () => {
    const sum = 0.1 + 0.2
    expect(sum).toBeCloseTo(0.3)
})
複製代碼

上面的測試結果以下:

4.3.5 字符串匹配toMatch

這個matcher就是用來判斷字符串是否和toMatch提供的模式匹配,以下:

// 字符串相關
test('toMatch', () => {
    const str = 'Lebron James'
    expect(str).toMatch(/Ja/)
    expect(str).toMatch('Ja')
})
複製代碼

4.3.6 數組,集合相關的matchers

可使用toContain判斷數組或者集合是否包含某個元素,使用toHaveLength判斷數組的長度,代碼以下:

test('Array Set matchers', () => {
    const arr = ['Kobe', 'James', 'Curry']
    const set = new Set(arr)
    expect(arr).toContain('Kobe')
    expect(set).toContain('Curry')
    expect(arr).toHaveLength(3)
})
複製代碼

4.3.7 異常相關的matchers

使用toThrow來判斷拋出的異常是否符合預期:

function throwError () {
    throw new Error('this is an error!!')
}
test('toThrow', () => {
    expect(throwError).toThrow(/this is an error/)
})
複製代碼

5. jest進階用法

5.1 分組測試與勾子函數

所謂分組測試,核心在於,將不一樣的測試進行分組,再結合勾子函數(生命週期函數),完成不一樣分組的定製化測試,以知足測試過程重的複雜需求。

咱們首先在src下新建兩個文件hook.jshook.test.js,這一部分代碼在這兩個文件中完成,首先直接給出hook.js代碼

// hook.js
export default class Count {
    constructor () {
        this.count = 2
    }
    increase () {
        this.count ++
    }

    decrease () {
        this.count --
    }

    double () {
        this.count *= this.count
    }

    half () {
        this.count /= this.count
    }
} 
複製代碼

如今呢,咱們想要對Count類的四個方法單獨測試,數據互相不影響,固然咱們能夠本身去直接實例化4個對象,不過,jest給了咱們更優雅的寫法---分組,咱們使用describe函數分組,以下:

describe('分別測試Count的4個方法', () => {
    test('測試increase', () => {
        
    })
    test('測試decrease', () => {
        
    })
    test('測試double', () => {
        
    })
    test('測試half', () => {
        
    })
})
複製代碼

這樣咱們就使用describe函數配合test將測試分爲了四組,接下來,爲了能更好的控制每一個test組,咱們就要用到jest的勾子函數。 咱們這裏要介紹的是jest裏的四個勾子函數beforeEach,beforeAll,afterEach,afterAll

顧名思義,beforeEach是在每個test函數執行以前,會被調用;afterEach則是在每個test函數執行以後調用;beforeAll是在全部test函數執行以前調用;afterAll則是在全部test函數執行以後調用。咱們能夠看下面這個例子:

import Count from "./hook"

describe('分別測試Count的4個方法', () => {
    let count
    beforeAll(() => {
        console.log('before all tests!')
    })

    beforeEach(() => {
        console.log('before each test!')
        count = new Count()
    })

    afterAll(() => {
        console.log('after all tests!')
    })

    afterEach(() => {
        console.log('after each test!')
    })

    test('測試increase', () => {
        count.increase()
        console.log(count.count)
    })
    test('測試decrease', () => {
        count.decrease()
        console.log(count.count)
    })
    test('測試double', () => {
        count.double()
        console.log(count.count)
    })
    test('測試half', () => {
        count.half()
        console.log(count.count)
    })
})
複製代碼

輸出的結果如圖:

能夠看到,咱們在每一個test執行以前,beforeEach裏面從新實例化了count,因此每一次的count是不一樣的。合理的使用勾子函數,咱們能夠更好的定製測試。

5.2 異步代碼測試之定時器

在咱們前端開發過程當中,因爲javascript是單線程的,異步編程是咱們開發人員常常要作的工做,而異步代碼也是最容易出錯的地方,對異步代碼邏輯進行測試,是頗有必要的,這一節將對jest如何進行異步測試,作一個詳細的介紹。

5.2.1 從最簡單的setTimeout開始

咱們首先新建timeout.js,timeout.test.js文件,timeout.js文件代碼很簡單:

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

咱們如今的目標就是去測試,寫的這個函數,是否是會像咱們預期的那樣,傳入一個函數做爲參數(簡單爲主,沒有作參數校驗),2s後,執行這個函數。

咱們的測試代碼(timeout.test.js)以下:

import timeout from './timeout'

test('測試timer', () => {
    timeout(() => {
        expect(2+2).toBe(4)
    })
})

複製代碼

若是咱們運行這段測試代碼,必定是會經過的,可是,這真的表明咱們寫在timeout裏的方法測試經過了嗎?咱們在timout.js中打印輸出一段文字

export default (fn) => {
    setTimeout(() => {
       fn()
       console.log('this is timeout!')
    }, 2000)
}
複製代碼

而後咱們運行測試代碼(npm test timeout.test這樣只運行一個文件),你會發現,什麼打印內容都沒有輸出:

其實產生這種現象的緣由也很簡單, jest在運行測試代碼,執行 test方法時,從函數內部第一行執行到最後一行,當執行邏輯走到代碼塊最後一行時,沒有異常就會返回測試成功,這個過程當中 不會去等待異步代碼的執行結果,因此咱們這樣的測試方法,無論 setTimeout裏怎麼實現,回調函數裏怎麼實現,都不會執行回調函數內部的邏輯。

若是咱們須要測試代碼在真正執行了定時器裏的異步邏輯後,才返回測試結果,咱們須要給test方法的回調函數傳入一個done參數,並在test方法內異步執行的代碼中調用這個done方法,這樣,test方法會等到done所在的代碼塊內容執行完畢後才返回測試結果:

import timeout from './timeout'

test('測試timer', (done) => {
    timeout(() => {
        expect(2+2).toBe(4)
        done()
    })
})
複製代碼

咱們能夠看到,增長 done參數以後,獲得了預期的結果,打印輸出了內容,證實咱們回調函數內的代碼執行了。

5.2.2 使用fakeTimers提升測試效率

咱們上一小節介紹瞭如何去測試寫在定時器裏異步代碼的執行,但這裏存在一個問題,好比,咱們的定時器可能須要幾十秒才執行內部邏輯(這雖然不多見,主要看業務需求),咱們的測試代碼也會好久纔會返回結果,這無疑大大的下降了開發測試效率。

jest也考慮到了這一點,讓咱們可使用fakeTimers模擬真實的定時器。這個fakeTimers在遇到定時器時,容許咱們當即跳過定時器等待時間,執行內部邏輯,好比,對於剛剛的timeout.test,咱們的測試代碼能夠作以下改變:

  1. 首先,咱們使用jest.fn()生成一個jest提供的用來測試的函數,這樣咱們以後回調函數不須要本身去寫一個
  2. 其次,咱們使用jest.useFakeTimers()方法啓動fakeTimers
  3. 最後,咱們能夠經過jest.advanceTimersByTime()方法,參數傳入毫秒時間,jest會當即跳過這個時間值,還能夠經過toHaveBeenCalledTimes()這個mathcer來測試函數的調用次數。

完整代碼以下:

test('測試timer', () => {
    jest.useFakeTimers()
    const fn = jest.fn()
    timeout(fn)
    // 時間快進2秒
    jest.advanceTimersByTime(2000)
    expect(fn).toHaveBeenCalledTimes(1)
})
複製代碼

咱們依然得到了預期的測試結果,注意觀察輸出結果裏 測試timer(12ms)對比以前的 測試timer(2021ms),能夠看到,定時器的延遲時間,確實被跳過了,這提升了測試開發效率。

5.2.3 更復雜的定時器場景

通過前面兩節的介紹,對於定時器這種異步場景的測試代碼編寫,實際上咱們已經掌握核心內容,這一節,咱們去探討一個更爲複雜的場景,那就是定時器嵌套。

咱們首先改造timout裏的代碼以下:

export default (fn) => {
    setTimeout(() => {
       fn()
       console.log('this is timeout outside!')
       setTimeout(() => {
            fn()
           console.log('this is timeout inside!')
       }, 3000)
    }, 2000)
}
複製代碼

按照上一小節的寫法,咱們的測試代碼能夠改造爲:

test('測試timer', () => {
    jest.useFakeTimers()
    const fn = jest.fn()
    timeout(fn)
    // 時間快進2秒
    jest.advanceTimersByTime(2000)
    expect(fn).toHaveBeenCalledTimes(1)
    // 時間快進3秒
    jest.advanceTimersByTime(3000)
    expect(fn).toHaveBeenCalledTimes(2)
})
複製代碼

其實也很簡單,就是在第一次2s後,再過3s後執行第二個定時器,此時fn被調用了2次,因此咱們只須要加上最後兩行代碼就能夠了。執行結果以下:

咱們能夠看到,兩條打印結果都輸出了。可是目前的這種實現不是很好,試想一下,若是這裏面的定時器嵌套比較多,或者咱們不清楚有幾個定時器,就會比較麻煩。jest爲這種狀況提供了兩個有用的方法:

  1. jest.runAllTimers()

這個方法就如同它的名字同樣,調用以後,會執行全部定時器,咱們的代碼能夠改造以下:

test('測試timer', () => {
    jest.useFakeTimers()
    const fn = jest.fn()
    timeout(fn)
    jest.runAllTimers()
    expect(fn).toHaveBeenCalledTimes(2)
})
複製代碼

能夠看到,兩個定時器內部的打印都輸出了,並且 jest依舊快速的跳過了定時器等待時間。

  1. jest.runOnlyPendingTimers()

這個方法的意思是,只執行當前正在等待的全部定時器,這個例子中,只有外層定時器是正在等待的,內層定時器只有在外層定時器執行時,才處於等待狀態,咱們改造測試代碼以下:

test('測試timer', () => {
    jest.useFakeTimers()
    const fn = jest.fn()
    timeout(fn)
    jest.runOnlyPendingTimers()
    expect(fn).toHaveBeenCalledTimes(1)
})
複製代碼

能夠看到,只有外層定時器裏的內容被打印輸出了。若是咱們想要繼續輸出內部定時器的內容,由於此時內部定時器處於等待狀態,因此再次執行 jest.runOnlyPendingTimers()便可。

關於上述內容,有一點須要說明:

若是咱們編寫了多個test函數,它們都使用fakeTimers,必定要在beforeEach勾子中每次都調用jest.useFakeTimers(),不然,多個test函數中的fakeTimers會是同一個,將會互相干擾,產生不符合預期的執行結果

beforeEach(() => {
    jest.useFakeTimers()
})
複製代碼

5.3 異步代碼測試之數據請求(promise/async await)

5.3.1 傳統的promise寫法

在咱們前端開發中,經過請求後端接口獲取數據是很重要的一個流程,這一節主要就是介紹這個過程當中如何編寫測試代碼(實際上這裏的不少內容,以前介紹定時器的章節是有介紹過的)

爲了簡單起見,咱們使用axios(npm i axios)這個成熟的庫來輔助咱們作數據請求。首先新建request.js, request.test.js這兩個文件,在request.js文件請求一個免費api:

import axios from 'axios'

export const request = fn => {
    axios.get('https://jsonplaceholder.typicode.com/todos/1').then(res => {
        fn(res)
        console.log(res)
    })
}
複製代碼

咱們在request.test.js中,爲了保證異步代碼執行完畢後結束測試,和以前介紹的同樣,在test的回調函數中傳入done參數,在回調函數裏執行done(),代碼以下:

import { request } from './request'

test('測試request', (done) => {
    request(data => {
        expect(data.data).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
          })
        done()
    })
})
複製代碼

咱們能夠看到,打印出了請求回來的內容,測試代碼也正確的拿到數據,測試經過。

咱們如今改造一下request.js的代碼,讓它返回一個promise:

export const request = () => {
    return axios.get('https://jsonplaceholder.typicode.com/todos/1')
}
複製代碼

爲了測試上述代碼,咱們request.test.js也要作必定的修改:

test('測試request', () => {
    return request().then(data => {
        expect(data.data).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
          })
    })
})
複製代碼

注意,上面的寫法不須要傳入done參數了,可是,須要咱們使用return返回,若是不寫return,那jest執行test函數時,將不會等待promise返回,這樣的話,測試結果輸出時,then方法將不會執行。咱們能夠嘗試如下兩種寫法(改變"completed": true),第一種寫法測試不會經過,第二種測試是能夠經過的(由於promise並無返回結果):

// 第一種
test('測試request', () => {
    return request().then(data => {
        expect(data.data).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": true
          })
    })
})
複製代碼

// 第二種
test('測試request', () => {
    request().then(data => {
        expect(data.data).toEqual({
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": true
          })
    })
})
複製代碼

上面的測試代碼,咱們也能夠寫成下面的形式:

test('測試request', () => {
    return expect(request()).resolves.toMatchObject({
        data: {
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
          }
    })
})
複製代碼

注意,resolves將返回request方法執行後全部返回內容,咱們使用toMatchObject這個matcher,當傳入的對象可以匹配到request方法執行後返回對象的一部分鍵值對,測試就會經過。

5.3.2 使用async await語法糖

async await本質上就是promise鏈式調用的語法糖,咱們上一小節最後的測試代碼,若是使用async await的方式去書寫,以下:

// 寫法一
test('測試request', async () => {
    const res = await request()
    expect(res.data).toEqual({
        "userId": 1,
        "id": 1,
        "title": "delectus aut autem",
        "completed": false
    })
})
// 寫法二
test('測試request', async () => {
    await expect(request()).resolves.toMatchObject({
        data: {
            "userId": 1,
            "id": 1,
            "title": "delectus aut autem",
            "completed": false
            }
        })
})
複製代碼

咱們上述兩種寫法都是能夠經過測試的。

5.3.3 對於請求出現錯誤的測試

在咱們實際項目中,須要對這種接口請求作錯誤處理,一樣,也須要對異常狀況編寫測試代碼。

咱們首先在request.js增長一個方法:

export const requestErr = fn => {
    return axios.get('https://jsonplaceholder.typicode.com/sda')
}
複製代碼

這裏請求一個不存在的接口地址,會返回404,因而咱們的的測試代碼爲:

test('測試request 404', () => {
    return requestErr().catch((e) => {
        console.log(e.toString())
        expect(e.toString().indexOf('404') > -1).toBeTruthy()
    })
})
複製代碼

這裏有個地方須要注意一下,下圖是 jest官網的一段說明:

大概意思就是說,若是測試代碼裏使用 catch,jest不回去執行 catch裏的內容,因此須要咱們去寫 expect.assertions(1)這句話,表明,指望執行的斷言是1次, catch方法算一次斷言,因此,正常狀況,因爲不會執行 catch,這裏會報錯(執行了0次斷言),當這裏報錯了,說明咱們的代碼也按照預期產生了異常。

這種寫法目前已經不須要了,詳細見removed useless expect.assertions,因此,如今就按照上面那種方式,直接書寫,測試經過表明確實如咱們預期的產生異常。

一樣的,咱們還可使用另外一種方式完成異常代碼測試:

test('測試request 404', () => {
    return expect(requestErr()).rejects.toThrow(/404/)
})
複製代碼

這裏的rejects和上一節的resolves相互對於,表明執行方法產生的錯誤對象,這個錯誤對象拋出404異常(toThrow(/404/))

咱們一樣可使用async await語法糖書寫異常測試的代碼:

test('測試request 404', async () => {
    await expect(requestErr()).rejects.toThrow(/404/)
})
// 或者可使用try catch語句寫的更完整
test('測試request 404', async () => {
    try {
        await requestErr()
    } catch (e) {
        expect(e.toString()).toBe('Error: Request failed with status code 404')
    }
})
複製代碼

5.4 在測試中模擬(mock)數據

咱們首先新建mock.js, mock.test.js文件

5.4.1 使用jest.fn()模擬函數

首先在mock.js寫一個函數:

export const run = fn => {
   return fn('this is run!')
}
複製代碼

實際上以前咱們已經使用過jest.fn()了,這裏咱們更進一步的學習它。

  1. 首先,咱們的fn()函數能夠接受一個函數做爲參數,這個函數就是咱們想要jest.fn()爲咱們mock的函數,咱們編寫mock.test.js
test('測試 jest.fn()', () => {
    const fn = jest.fn(() => {
        return 'this is mock fn 1'
    })
})
複製代碼
  1. 其次,jest.fn()能夠初始化時候不傳入參數,而後經過調用生成的mock函數的mockImplementation或者mockImplementationOnce方法來改變mock函數內容,這兩個方法的區別是,mockImplementationOnce只會改變要mock的函數一次:
test('測試 jest.fn()', () => {
    const func = jest.fn()
    func.mockImplementation(() => {
        return 'this is mock fn 1'
    })
    func.mockImplementationOnce(() => {
        return 'this is mock fn 2'
    })
    const a = run(func)
    const b = run(func)
    const c = run(func)
    console.log(a)
    console.log(b)
    console.log(c)
})
複製代碼

咱們能夠看到,函數執行的結果第一次是 this is mock fn 2,以後都是 this is mock fn 1

一樣的,咱們可使用mock函數的mockReturnValuemockReturnValueOnce(一次)方法來改變函數的返回值:

test('測試 jest.fn()', () => {
    const func = jest.fn()
    func.mockImplementation(() => {
        return 'this is mock fn 1'
    })
    func.mockImplementationOnce(() => {
        return 'this is mock fn 2'
    })
    func.mockReturnValue('this is mock fn 3')
    func.mockReturnValueOnce('this is mock fn 4')
        .mockReturnValueOnce('this is mock fn 5')
        .mockReturnValueOnce('this is mock fn 6')
    const a = run(func)
    const b = run(func)
    const c = run(func)
    const d = run(func)
    console.log(a)
    console.log(b)
    console.log(c)
    console.log(d)
})
複製代碼

注意到,方法是能夠鏈式調用的,方便屢次輸出不一樣的返回值。

  1. 最後,咱們可使用toBeCalledWith這個matcher來測試函數的傳參數是否符合預期:
test('測試 jest.fn()', () => {
    const func = jest.fn()
    const a = run(func)
    expect(func).toBeCalledWith('this is run!')
})
複製代碼

5.4.2 模擬接口中獲取的數據

不少時候,咱們在前端開發過程當中,後端接口尚未提供,咱們須要去mock接口返回的數據。

咱們首先在mock.js中編寫一個簡單的請求數據的代碼:

import axios from 'axios'

export const request = fn => {
    return axios.get('https://jsonplaceholder.typicode.com/todos/1')
}
複製代碼

接着,咱們在mock.test.js中,使用jest.mock()方法模擬axios,使用mockResolvedValuemockResolvedValueOnce方法模擬返回的數據,一樣的,mockResolvedValueOnce方法只會改變一次返回的數據:

import axios from 'axios'
import { request } from './mock'

jest.mock('axios')

test('測試request', async () => {
    axios.get.mockResolvedValueOnce({ data: 'Jordan', position: 'SG' })
    axios.get.mockResolvedValue({ data: 'kobe', position: 'SG' })
    await request().then((res) => {
        expect(res.data).toBe('Jordan')
    })
    await request().then((res) => {
        expect(res.data).toBe('kobe')
    })
})
複製代碼

咱們使用jest.mock('axios')來使用jest去模擬axios,測試正確的經過了。

5.5 dom相關測試

dom相關的測試其實很簡單,咱們首先新建dom.js, dom.test.js兩個文件,代碼以下:

// dom.js
export const generateDiv = () => {
    const div = document.createElement('div')
    div.className = 'test-div'
    document.body.appendChild(div)
}

// dom.test.js
import { generateDiv } from './dom'

test('測試dom操做', () => {
    generateDiv()
    generateDiv()
    generateDiv()
    expect(document.getElementsByClassName('test-div').length).toBe(3)
})
複製代碼

這裏只有一點要說明,jest的運行環境是node.js,這裏jest使用jsdom來讓咱們能夠書寫dom操做相關的測試邏輯。

5.6 快照(snapshot)測試

咱們若是沒有接觸過快照測試,可能會以爲這個名字很高大上。因此咱們首先新建snapshot.js, shapshot.test.js來看看快照測試到底是什麼。

在咱們的平常開發中,總會寫一些配置性的代碼,它們大致不會變化,可是也會有小的變動,這樣的配置可能以下(snapshot.js):

export const getConfig = () => {
    return {
        server: 'https://demo.com',
        port: '8080'
    }
}
複製代碼

咱們的測試代碼以下:

import { getConfig } from './snapshot'

test('getConfig測試', () => {
    expect(getConfig()).toEqual({
        server: 'https://demo.com',
        port: '8080'
    })
})
複製代碼

這樣咱們經過了測試。可是,假如後續咱們的配置改變了,我就須要同步的去修改測試代碼,這樣會比較麻煩,從而,jest爲咱們引入了快照測試,先上測試代碼:

test('getConfig測試', () => {
    expect(getConfig()).toMatchSnapshot()
})
複製代碼

咱們運行測試代碼以後,會在項目根目錄下生成一個__snapshots__文件夾,下面有一個snapshot.test.js.snap快照文件,文件內容以下:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`getConfig測試 1`] = `
Object {
  "port": "8080",
  "server": "https://demo.com",
}
`;
複製代碼

jest會在運行toMatchSnapshot()的時候,首先檢查有沒有這個快照文件,若是沒有,則生成,當咱們改動配置內容時,好比把port改成8090,再次運行測試代碼,測試不經過,結果以下:

這個時候,咱們只須要運行 npm test snapshot.test -- -u,就能夠自動更新咱們的快照文件,測試再次經過,這就讓咱們不須要每次更改配置文件的時候,手動去同步更新測試代碼,提升了測試開發效率:

此時咱們的快照文件更新爲以下代碼:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`getConfig測試 1`] = `
Object {
  "port": "8090",
  "server": "https://demo.com",
}
`;
複製代碼

6. jest其它一些有用的知識

6.1 讓jest監聽文件變化

這個功能很簡單,咱們只須要運行jest命令的時候,後面加上--watch便可,咱們在package.json中新增一條命令:

"scripts": {
    "test": "jest",
    "test-watch": "jest --watch"
},
複製代碼

在新增完這條命令後,爲了能讓jest能夠監聽文件變化,咱們還須要把咱們的代碼文件變成一個git倉庫,jest也正式依靠git的能力實現監聽文件變化的,咱們運行git init,接着咱們運行npm run test-watch,在必定時間後,咱們開啓監聽模式,命令行最後幾行輸出應該是:

這裏對watch模式的幾個有用功能作一個簡單介紹(也就是圖中英文說明):

  1. a鍵運行全部測試代碼
  2. f鍵只運行全部失敗的測試代碼
  3. p鍵按照文件名篩選測試代碼(支持正則)
  4. t鍵按照測試名篩選測試代碼(支持正則)
  5. q鍵盤推出watch模式
  6. enter鍵觸發一次測試運行

這些我建議你們自行去嘗試,它們都是十分簡單好用的功能。

6.2 生成測試覆蓋率文件

測試覆蓋率,簡單來講就是咱們業務代碼中,編寫測試代碼的比例,jest給咱們提供了直接生成測試覆蓋率文件的方法,也就是運行jest命令時後面加上--coverage參數,咱們修改package.json文件以下:

"scripts": {
    "test": "jest",
    "test-watch": "jest --watch",
    "coverage": "jest --coverage"
},
複製代碼

接下來,運行npm run coverage,咱們能夠看到命令行輸出以下:

這是一份測試覆蓋率表格。同時,咱們發現,文件夾下自動生成了一個 coverage文件夾:

咱們在瀏覽器中運行 index.html,以下圖:

這個頁面向咱們展現了項目中不一樣文件的測試覆蓋率,咱們能夠點擊不一樣文件名字進入查看具體一個文件中,哪些代碼被測試到了,哪些沒有被測試到。

這裏對這個表格項目作一個簡單的說明:

  1. Statements是語句覆蓋率:表示代碼中有多少執行的語句被測試到了

  2. Branches是分支覆蓋率:表示代碼中有多少if else switch分支被測試到了

  3. Functions是函數覆蓋率:表示代碼中被測試到的函數的佔比

  4. Lines是行覆蓋率:表示代碼中被測試到的行數佔比

咱們能夠利用生成的測試覆蓋率文件,更好的完善改進咱們的測試代碼。

6.3 關於jest.config.js配置文件

我對於學習一個工具的配置文件的建議是,首先按照默認的來,當你須要改變配置的時候,再去查閱官方文檔學習,不推薦去死記硬背。

我這裏也不會去介紹怎麼去配置jest文件,咱們能夠經過jest初始化時候默認生成的那個jest.config.js來學習(有詳細註釋),也能夠在官網中查閱相關的配置參數。

7. 寫在最後

因爲篇幅緣由,不適合再介紹更多的信息,更多的api相關的信息,建議去查閱官網來學習。

這篇文章我的認爲已經把jest的基礎和最核心的內容作了闡述,可能咱們開發過程當中,使用react(enzyme), vue( @vue/test-utils)這樣的開發框架,使用webpack這樣的工程化工具,在使用jest的時候,會結合使用一些開源庫,我相信學好了jest自己以後,配置和使用它們都不會有太多困難。

最後,但願這篇文章能夠幫助到你們,感謝能看到這裏的每個小夥伴。

相關文章
相關標籤/搜索