前端自動(dòng)化測(cè)試Jest基礎(chǔ)學(xué)習(xí)記錄

jest 自動(dòng)測(cè)試

安裝

npm install jest@24.8.0 -D
jest使用需要模塊化機(jī)制。
配置npm script命令,package.json文件配置:

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

單元測(cè)試:?jiǎn)蝹€(gè)模塊進(jìn)行測(cè)試
集成測(cè)試:多個(gè)模塊進(jìn)行測(cè)試
jest配置: npx jest --init
jest配置文件:jest.config.js 參數(shù):coverageDirectory:生成的覆蓋率文件夾存檔地址
代碼測(cè)試覆蓋率:使用jest測(cè)試過的代碼占所需測(cè)試代碼百分比 npx jest --coverage 生成coverage文件夾內(nèi)部包含測(cè)試文件覆蓋率文件

如何使用es6 module ?
使用babel :npm install @babel/core@7.4.5 @babel/preset-env@7.4.5 -D
.babelrc配置

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

jest默認(rèn)使用commonjs規(guī)范,使用es6module運(yùn)行過程為:

  • npm run jest
  • jest(babel-jest)
  • 檢測(cè)是否有安裝babel模塊
  • 在運(yùn)行測(cè)試之前結(jié)合babel,先把代碼進(jìn)行一次轉(zhuǎn)化
  • 運(yùn)行轉(zhuǎn)化過得測(cè)試用例代碼

測(cè)試用例

匹配器
真假相關(guān)
toBe():使用Object.is()實(shí)現(xiàn)精確匹配
toEqual():遞歸檢查對(duì)象或數(shù)組的每一個(gè)字段。
toBeNull():判斷是否為null
toBeUndefined():判斷是否是undefined
toBeDefined():判斷是否是定義過的
toBeTruthy():判斷是否為真
toBeFalsy():判斷是否為假
not:取反匹配器 expect(a).not.toBeFalsy()

數(shù)字相關(guān)
toBeGreaterThan()
toBeLessThan()
toBeGreaterThanOrEqual()
toBeLessThanOrEqual();
toBeCloseTo(); 比較浮點(diǎn)數(shù)計(jì)算的時(shí)候

test("目標(biāo)數(shù)是否大于某個(gè)數(shù)字",()=>{
    expect(12).toBeGreaterThan(13)
})

字符串相關(guān)
toMatch():參數(shù)可以是=字符串也可以是正則

test("判斷字符串是否包含某內(nèi)容",()=>{
    var str = 'abcde'
    expect(str).toMatch('a')
})

數(shù)組 Set
toContain()

test("判斷數(shù)組是否包含某項(xiàng)",()=>{
    var arr = [1,2,3];
    expect(arr).toContain(2)
})

異常
toThrow():參數(shù)可以為字符串也可為正則

const throwNewError = function(){
    throw new Error("this is an error")
}
test("拋出異常",()=>{
    expect(throwNewError).toThrow("this is an error");
})

jest命令行

w 進(jìn)入選擇模式
f 只運(yùn)行失敗的測(cè)試
a 每次將所有的測(cè)試用例都跑一次
o 只運(yùn)行修改之后的測(cè)試文件(多個(gè)文件時(shí),需要配合git使用)

  "scripts": {
    "test": "jest --watch" // watch表示默認(rèn)開啟o模式
  },

t 根據(jù)一個(gè)測(cè)試用例名字正則表達(dá)式來過濾需要run的測(cè)試用例(可以理解為過濾模式)

p 配合matchAll使用 根據(jù)文件名過濾掉不需要測(cè)試的文件

異步測(cè)試

1. 回調(diào)類型異步函數(shù)測(cè)試
fetchData.js

export const fetchData =(fn)=> {
    axios.get("http://www.dell-lee.com/react/api/demo111.json")
    .then((response)=>{
        fn(response.data)
    });
}

fetchData.test.js

import {fetchData} from './fetchData.js';
test('測(cè)試fetchData函數(shù)返回結(jié)果為{success : true}',(done)=>{
    fetchData((data)=>{
        expect(data).toEqual({
            success:true
        })
        done()
    })
})

注意點(diǎn):需要在test方法第二個(gè)參數(shù)中傳入done函數(shù)作為參數(shù),并在異步方法執(zhí)行完成之后再執(zhí)行,即可正確進(jìn)行異步測(cè)試。

2. 直接返回promise對(duì)象異步測(cè)試
fetchData.js

export const fetchData =()=> {
    return axios.get("http://www.dell-lee.com/react/api/demo111.json")
}

fetchData.test.js

test("測(cè)試fetchDate返回結(jié)果是{success:true}",()=>{
    return fetchData()
    .then((response)=>{
        expect(response.data).toEqual({
            success:true
        })
    })
})

注意點(diǎn):當(dāng)fetchData函數(shù)返回一個(gè)promise對(duì)象的時(shí)候必須將執(zhí)行結(jié)果return出去。

案例:就需要判斷必須是404

test("測(cè)試fetchData返回結(jié)果為404",()=>{
    return fetchData()
    .catch((e)=>{
        expect(e.toString().indexOf('404')>-1).toBe(true)
    })
})

注意:當(dāng)fetchData函數(shù)請(qǐng)求結(jié)果有正常數(shù)據(jù),那么就不會(huì)走catch方法,也就不會(huì)走expect(e.toString().indexOf('404')>-1).toBe(true)測(cè)試,那么測(cè)試結(jié)果默認(rèn)為通過。因此需要補(bǔ)一句expect.assertions(1),表示,在下面必須再執(zhí)行一條expect方法,否則就當(dāng)該測(cè)試用例不通過。代碼如下:

test("測(cè)試fetchData返回結(jié)果為404",()=>{
    expect.assertions(1)
    return fetchData()
    .catch((e)=>{
        expect(e.toString().indexOf('404')>-1).toBe(true)
    })
})
  • 使用resolves + toMatchObject()匹配器測(cè)試
export const fetchData =()=> {
    return axios.get("http://www.dell-lee.com/react/api/demo.json")
}
test("測(cè)試返回結(jié)果中是否包含data:{success:true}這個(gè)對(duì)象",()=>{
    return expect(fetchData()).resolves.toMatchObject({
        data:{
            success:true
        }
    })
})

注意:必須顯式的return出結(jié)果,主要是判斷fetchData返回結(jié)果中是否包含data:{success:true}這個(gè)對(duì)象。

  • 使用reject + toThrow()匹配器檢測(cè)拋出異常
export const fetchData =()=> {
    return axios.get("http://www.dell-lee.com/react/api/demo1.json")
}
test("使用toThrow()檢測(cè)拋出異常",()=>{
    return expect(fetchData()).rejects.toThrow()
})
  • 使用async/await測(cè)試異步
export const fetchData =()=> {
    return axios.get("http://www.dell-lee.com/react/api/demo1.json")
}
test("測(cè)試fetchData返回結(jié)果為404", async ()=>{
    await expect(fetchData()).resolves.toMatchObject({
        data:{
            success:true
        }
    })
})
test("測(cè)試fetchData返回結(jié)果為404", async ()=>{
    await expect(fetchData()).rejects.toThrow()
})

注意:使用async/await可以代替使用return顯示返回promise對(duì)象,但是需要注意的是async需要和await配合使用。

  • 使用await先獲取代碼執(zhí)行結(jié)果再做后續(xù)判斷
export const fetchData =()=> {
    return axios.get("http://www.dell-lee.com/react/api/demo.json")
}
test("測(cè)試fetchData返回結(jié)果包含data:{success:true}", async ()=>{
    const data = await fetchData();
    expect(data).toMatchObject({
        data:{
            success:true
        }
    })
})
test("測(cè)試fetchData返回結(jié)果為404", async ()=>{
    expect.assertions(1);
    try{
        await fetchData();
    }catch(e){
        expect(e.toString()).toMatch("404")
    }    
})

注意:在測(cè)試請(qǐng)求不通過的情況下需要使用try catch捕獲錯(cuò)誤,并且搭配expect.assertions()使用防止請(qǐng)求成功時(shí)不走catch邏輯,從而也顯示測(cè)試通過。

Jest鉤子函數(shù)

在jest執(zhí)行過程中自動(dòng)執(zhí)行的函數(shù)

beforeAll():表示在所有測(cè)試用例執(zhí)行之前執(zhí)行一次。
afterAll():表示在所有測(cè)試用例執(zhí)行完成之后執(zhí)行一次。
beforeEach():表示在每個(gè)測(cè)試用例執(zhí)行之前都會(huì)執(zhí)行一次。
afterEach():表示在每個(gè)測(cè)試用例執(zhí)行完成之后都會(huì)執(zhí)行一次。

描述方法,可以歸類所有的測(cè)試用例

describe('描述', () => {
    //測(cè)試用例
})

描述方法可以嵌套使用

describe("測(cè)試用例", () => {
    describe("測(cè)試加法",() => {
        //所有關(guān)于加法的測(cè)試用例
    })
    describe("測(cè)試減法",() => {
        //所有關(guān)于減法的測(cè)試用例
    })
})

demo:

//Counter.js

class Counter {
    constructor(){
        this.count = 1;
    }
    addOne(){
        this.count += 1;
    }
    minusOne(){
        this.count -= 1
    }
    addTwo(){
        this.count += 2;
    }
    minusTwo(){
        this.count -= 2
    }
}

export default Counter;
Counter.test.js
import Counter from './Counter';
let counter = null;
beforeAll(() => {
    console.log("beforeAll")
})

beforeEach(() => {
    counter = new Counter();
    console.log("beforeEach")
})

afterEach(() => {
    console.log("afterEach")
})

afterAll(() => {
    console.log("afterAll")
})

describe("counter 測(cè)試代碼", () => {
    describe("加法測(cè)試", () => {
        test('測(cè)試counter +1 ', () => {
            counter.addOne();
            expect(counter.count).toBe(2)
        })
        test("測(cè)試counter +2", () => {
            counter.addTwo();
            expect(counter.count).toBe(3)
        })
    })
    describe("減法測(cè)試", () => {
        test("測(cè)試counter -1 ", () => {
            counter.minusOne();
            expect(counter.count).toBe(0)
        })
        test("測(cè)試counter -2", () => {
            counter.minusTwo();
            expect(counter.count).toBe(-1);
        })
    })
})

鉤子函數(shù)的作用域

每個(gè)describe方法都會(huì)對(duì)jest的鉤子函數(shù)產(chǎn)生一個(gè)作用域。比如:

describe("counter 測(cè)試", () => {
    beforeAll(() => {
        console.log("beforeAll")
    })
    
    beforeEach(() => {
        counter = new Counter();
        console.log("beforeEach")
    })
    
    afterEach(() => {
        console.log("afterEach")
    })
    
    afterAll(() => {
        console.log("afterAll")
    })
    
    describe("加法測(cè)試", () => {
        beforeAll(() => {
            console.log("beforeAll 加法測(cè)試")
        })
        beforeEach(() => {
            counter = new Counter();
            console.log("beforeEach 加法測(cè)試")
        })
        test('測(cè)試counter +1 ', () => {
            counter.addOne();
            expect(counter.count).toBe(2)
        })
        test("測(cè)試counter +2", () => {
            counter.addTwo();
            expect(counter.count).toBe(3)
        })
    })
    describe("減法測(cè)試", () => {
        test("測(cè)試counter -1 ", () => {
            counter.minusOne();
            expect(counter.count).toBe(0)
        })
        test("測(cè)試counter -2", () => {
            counter.minusTwo();
            expect(counter.count).toBe(-1);
        })
    })
})

描述為“加法測(cè)試”的describe中的鉤子函數(shù)不會(huì)在“減法測(cè)試”中的describe去執(zhí)行。
執(zhí)行順序?yàn)橄葓?zhí)行外層的鉤子函數(shù),再執(zhí)行內(nèi)層的鉤子函數(shù),先執(zhí)行beforeAll()鉤子函數(shù),再執(zhí)行beforeEach()鉤子函數(shù)。

注意:測(cè)試用例初始化準(zhǔn)備的代碼一般都寫在鉤子函數(shù)中,不能直接寫在describe中,因?yàn)閐escribe中的代碼會(huì)優(yōu)先于jest鉤子函數(shù)執(zhí)行。比如:

describe("outer", ()=>{
    console.log("outer");
    beforeAll(()=>{
        console.log("outer beforeAll")
    })
    describe("inner",()=>{
        console.log("inner")
        beforeAll(()=>{
            console.log("inner beforeAll")
        }) 
        // ...test() 測(cè)試用例
    })
})

打印輸出順序?yàn)椋簅uter -> inner -> outer BeforeAll -> inner beforeAll

單個(gè)測(cè)試only
如果只想對(duì)其中一個(gè)測(cè)試用例進(jìn)行測(cè)試,而跳過其他測(cè)試用例可以使用only修飾符

describe("加法測(cè)試", () => {
    beforeAll(() => {
        console.log("beforeAll 加法測(cè)試")
    })
    beforeEach(() => {
        counter = new Counter();
        console.log("beforeEach 加法測(cè)試")
    })
    test.only('測(cè)試counter +1 ', () => {
        counter.addOne();
        expect(counter.count).toBe(2)
    })
    test("測(cè)試counter +2", () => {
        counter.addTwo();
        expect(counter.count).toBe(3)
    })
})

此時(shí)只會(huì)執(zhí)行“測(cè)試counter +1”這個(gè)測(cè)試用例,而跳過其他測(cè)試用例。

Jest中的Mock

  1. 使用jest.fn生成一個(gè)mock函數(shù),可以用來測(cè)試一個(gè)函數(shù)是否執(zhí)行(通過測(cè)試回調(diào)是否執(zhí)行來測(cè)試)。
// demo.js
export function Demo(cb){
    cb();
}
// demo.test.js
import { Demo } from './demo.js';
test("demo 中的回調(diào)是否執(zhí)行" , () => {
    let fn = jest.fn()
    Demo(fn)
    expect(fn).toBeCalled()
})

使用jest.fn()生成一個(gè)mock函數(shù),然后執(zhí)行Demo(fn),之后再使用toBeCalled()匹配器判斷Demo函數(shù)中的回調(diào)函數(shù)是否執(zhí)行,如果執(zhí)行說明Demo函數(shù)正常執(zhí)行,否則Demo函數(shù)中邏輯有錯(cuò)誤。

mock函數(shù)能幫我們干什么???

  • 捕獲函數(shù)的調(diào)用和返回結(jié)果以及this和調(diào)用順序。
  • 可以讓我們自由的設(shè)置返回結(jié)果
  • 改變函數(shù)的內(nèi)部實(shí)現(xiàn)(比如只模擬axios發(fā)送請(qǐng)求,而不去測(cè)試返回結(jié)果)
test("demo 中的回調(diào)是否執(zhí)行" , () => {
    let fn = jest.fn()
    Demo(fn)
    expect(fn).toBeCalled()
    console.log(fn.mock);//mock的函數(shù)會(huì)有一個(gè)mock屬性,屬性里面包括了fn被調(diào)用的情況
})

打印出結(jié)果為:

{
    calls: [ [] ],
    instances: [ undefined ],
    invocationCallOrder: [ 1 ],
    results: [ { type: 'return', value: undefined } ]
}

參數(shù)解讀:
calls:數(shù)組,length,表示該fn被調(diào)用了幾次,里面的每一項(xiàng)表示調(diào)用函數(shù)時(shí),傳入的參數(shù),比如calls:[["123"],["123"]]表示fn被調(diào)用2次,每次調(diào)用的時(shí)候傳遞的參數(shù)都是“123”,舉例:

test("demo 中的回調(diào)是否執(zhí)行" , () => {
    let fn = jest.fn()
    Demo(fn);
    Demo(fn);
    expect(fn.mock.calls.length).toBe(2)
})

可以使用fn.mock.calls.length判斷是否執(zhí)行。
還可以判斷調(diào)用參數(shù)是否是“123”:

test("demo 中的回調(diào)是否執(zhí)行" , () => {
    let fn = jest.fn()
    Demo(fn);
    Demo(fn);
    expect(fn.mock.calls[0]).toEqual(['123'])
})

invocationCallOrder:數(shù)組,表示被調(diào)用的順序

test("demo 中的回調(diào)是否執(zhí)行" , () => {
    let fn = jest.fn()
    Demo(fn);
    Demo(fn);
    Demo(fn);
    console.log(fn.mock.invocationCallOrder)
})

輸出[ 1, 2, 3 ]表示當(dāng)執(zhí)行3次Demo(fn)那么他會(huì)按照順序執(zhí)行。

results:數(shù)組,表示函數(shù)執(zhí)行之后每次的返回值是什么

test("demo 中的回調(diào)是否執(zhí)行" , () => {
    let fn = jest.fn(()=>{
        return "456"
    })
    Demo(fn);
    Demo(fn);
    Demo(fn);
    console.log(fn.mock.results);
})

在jest.fn()中使用函數(shù)返回一個(gè)字符串“456”然后輸出結(jié)果為

[
      { type: 'return', value: '456' },
      { type: 'return', value: '456' },
      { type: 'return', value: '456' }
]

在給fn添加返回值時(shí)還有其他方法
使用fn.mockReturnValueOnce(value)api,表示模擬一個(gè)返回值,但只模擬一次。

test("demo 中的回調(diào)是否執(zhí)行" , () => {
    let fn = jest.fn();
    fn.mockReturnValueOnce("123")
    Demo(fn);
    Demo(fn);
    Demo(fn);
    console.log(fn.mock.results);
})

輸出結(jié)果為:

[
    { type: 'return', value: '123' },
    { type: 'return', value: undefined },
    { type: 'return', value: undefined }
]

可以使用這個(gè)方法模擬多次不同返回值:

test("demo 中的回調(diào)是否執(zhí)行" , () => {
    let fn = jest.fn();
    fn.mockReturnValueOnce("123");
    fn.mockReturnValueOnce("456");
    fn.mockReturnValueOnce("789");
    Demo(fn);
    Demo(fn);
    Demo(fn);
    console.log(fn.mock.results);
})

輸出結(jié)果為:

[
    { type: 'return', value: '123' },
    { type: 'return', value: '456' },
    { type: 'return', value: '789' }
]

也可以鏈?zhǔn)秸{(diào)用:

test("demo 中的回調(diào)是否執(zhí)行" , () => {
    let fn = jest.fn();
    fn.mockReturnValueOnce("123").mockReturnValueOnce("456").mockReturnValueOnce("789");
    Demo(fn);
    Demo(fn);
    Demo(fn);
    console.log(fn.mock.results);
})

如果每次只需要返回同一個(gè)值,可以使用剛才在jest.fn()中去封裝一個(gè)方法,還可以使用fn.mockReturnValue(value)

test("demo 中的回調(diào)是否執(zhí)行" , () => {
    let fn = jest.fn();
    fn.mockReturnValue("hello")
    Demo(fn);
    Demo(fn);
    Demo(fn);
    console.log(fn.mock.results);
})

輸出結(jié)果為:

[
    { type: 'return', value: 'hello' },
    { type: 'return', value: 'hello' },
    { type: 'return', value: 'hello' }
]

結(jié)合fn.mockresults屬性可以寫其他的測(cè)試用例了
比如:

test("demo 中的回調(diào)是否執(zhí)行" , () => {
    let fn = jest.fn();
    fn.mockReturnValue("hello")
    Demo(fn);
    Demo(fn);
    Demo(fn);
    expect(fn.mock.results[0].value).toBe("hello");
})

測(cè)試結(jié)果為通過。

instances:數(shù)組,每項(xiàng)表示每次fn執(zhí)行的時(shí)候fn中this指向。

//demo.js
export function createObj(classItem){
    new classItem();
}

表示createObj方法接收一個(gè)類,在createObj方法中對(duì)類進(jìn)行實(shí)例化。

//demo.test.js
test.only('測(cè)試createObj方法',() => {
    let fn = jest.fn();
    createObj(fn);
    console.log(fn.mock)
})

輸出結(jié)果為:

{
    calls: [ [] ],
    instances: [ mockConstructor {} ],
    invocationCallOrder: [ 1 ],
    results: [ { type: 'return', value: undefined } ]
}

也就是表示fn方法中的this,指向的是jest.fn()的構(gòu)造函數(shù)mockConstructor;

小總結(jié):通過jest.fn()模擬出來的函數(shù)它有一個(gè)mock屬性,mock屬性中的calls表示該函數(shù)被調(diào)用的幾次,以及每次傳入的參數(shù),instances表示該函數(shù)被調(diào)用的幾次,以及每次調(diào)用該函數(shù)中this指向,invocationCallOrder表示該函數(shù)被調(diào)用的幾次,以及調(diào)用順序,resultes表示該函數(shù)被調(diào)用的幾次,以及每次調(diào)用的返回值。

改變函數(shù)的內(nèi)部實(shí)現(xiàn)指的是,比如前端在測(cè)試后臺(tái)接口返回?cái)?shù)據(jù)問題時(shí),不必測(cè)試接口返回了什么東西,只需要測(cè)試,前端是否發(fā)送ajax請(qǐng)求即可,返回值可以前端自己模擬一下。核心apimockResolvedValue;

//demo.js
export function getData(){
    return axios.get('http://www.dell-lee.com/react/api/demo.json').then((response)=>{
        return response.data;
    })
}
//demo.test.js
import { getData } from "./demo.js";
import axios from "axios";
jest.mock("axios");

test.only("測(cè)試 getData",async () => {
    axios.get.mockResolvedValue({data:"hello"}) //模擬axios get返回值
    await getData().then((data)=>{
        expect(data).toBe("hello") //確認(rèn)發(fā)起請(qǐng)求
    })
})

此段代碼表示,使用axios.get.mockResolvedValue({data:"hello"})方法模擬了axios的get方法返回值,當(dāng)調(diào)用getData()方法時(shí),請(qǐng)求回來的結(jié)果不是從服務(wù)器異步獲取到的,而是我們同步模擬的,因此可以節(jié)省時(shí)間,節(jié)省資源。

axios.get.mockResolvedValue()api也可以換成axios.get.mockResolvedValueOnce(),這個(gè)表示只模擬一次,當(dāng)發(fā)起多個(gè)請(qǐng)求的時(shí)候就會(huì)報(bào)測(cè)試不通過。
比如:

test.only("測(cè)試 getData",async () => {
    axios.get.mockResolvedValue({data:"hello"})
    await getData().then((data)=>{
        expect(data).toBe("hello")
    })
    await getData().then((data)=>{
        expect(data).toBe("hello")
    })
})

上面這段代碼會(huì)通過測(cè)試。
下面這段代碼不會(huì)通過測(cè)試。

test.only("測(cè)試 getData",async () => {
    axios.get.mockResolvedValueOnce({data:"hello"})
    await getData().then((data)=>{
        expect(data).toBe("hello")
    })
    await getData().then((data)=>{
        expect(data).toBe("hello")
    })
})

總結(jié)mock函數(shù)的作用:

    1. 捕獲函數(shù)的調(diào)用和返回結(jié)果,以及this和調(diào)用順序。
    1. 它可以讓我們自由地設(shè)置返回結(jié)果。
    1. 可以改變函數(shù)的內(nèi)部實(shí)現(xiàn)。

補(bǔ)充mock函數(shù)的一些語(yǔ)法。。。

改變函數(shù)返回值
第一種方法:直接在jest.fn()中去實(shí)現(xiàn)

test("demo 中的回調(diào)是否執(zhí)行" , () => {
    let fn = jest.fn(()=>{
        return "hello"    
    });
    Demo(fn);
    console.log(fn.mock.results)
})

第二種方法:使用fn.mockReturnValue()

test("demo 中的回調(diào)是否執(zhí)行" , () => {
    let fn = jest.fn();
    fn.mockReturnValue("hello");
    Demo(fn);
    console.log(fn.mock.results)
})

第三種方法:使用fn.mockImplementation()

test("demo 中的回調(diào)是否執(zhí)行" , () => {
    let fn = jest.fn();
    fn.mockImplementation(()=>{
        return "world"
    });
    Demo(fn);
    console.log(fn.mock.results)
})

還有fn.mockImplementationOnce()表示只模擬一次。

mockImplementationOnce()比較mockReturnValue()前者可以在里面的函數(shù)中去做一些其他邏輯操作,而后者只是一個(gè)返回結(jié)果。

返回this方法

fn.mockImplementationOnce(()=>{
    return this;
})

或者

fn.mockReturnThis()

toBeCalledWith()匹配器

expect(fn.mock.calls[0].toEqual(["abc"]))

等價(jià)于

expect(fn).toBeCalledWith("abc")

區(qū)別是前者表示第一次調(diào)用fn的參數(shù)是abc,后者表示每次調(diào)用fn參數(shù)都是abc

vsCode插件jest,可以不用手動(dòng)去執(zhí)行npm run test。這個(gè)插件會(huì)自動(dòng)去檢測(cè)測(cè)試用例是否執(zhí)行成功,并且會(huì)給與提示。

Snapshot(快照測(cè)試)

基礎(chǔ)使用
在測(cè)試配置文件的時(shí)候,當(dāng)頻繁修改配置文件時(shí),需要同步更新測(cè)試文件,為了避免這種情況,可以使用快照匹配器。

//snap.js
export const generateConfig = function(){
    return {
        name:"lisa",
        age:19,
        sex:"male",
        couple:true
    }
}
//snap.test.js
import { generateConfig } from './snap.js';
test("測(cè)試配置文件",()=>{   
    expect(generateConfig()).toMatchSnapshot();
})

快照測(cè)試過程:
第一次執(zhí)行測(cè)試命令時(shí)生成一個(gè)和配置文件一樣的快照文件(snapshots),對(duì)比快照文件和配置文件,相同則表示測(cè)試通過。
當(dāng)修改配置文件之后,再執(zhí)行測(cè)試命令,會(huì)拿新的配置文件快照去和之前的快照做對(duì)比,如果相同則通過測(cè)試,否則不會(huì)通過。
不會(huì)通過的時(shí)候會(huì)提示具體的原因,可以使用jest命令行w查看所有的指令,之后使用u,再用新的配置文件快照去更新快照,這樣再去進(jìn)行測(cè)試。

當(dāng)有多個(gè)配置文件需要進(jìn)行快照測(cè)試時(shí),使用u會(huì)更新所有的配置文件方法,因此可以使用i,每次只提示一個(gè)配置文件方法,然后使用u去更新當(dāng)前快照,之后在重復(fù)循環(huán)執(zhí)行i->u即可實(shí)現(xiàn)每次只修改一個(gè)文件機(jī)制。

當(dāng)一個(gè)配置文件中有日期(new Date())時(shí),內(nèi)次更新快照和之前的date都不一樣,所以,這種情況下如下處理:

export const generateConfig = function(){
    return {
        name:"lisa",
        age:40,
        sex:"female",
        couple:true,
        date:new Date()
    }
}
test("測(cè)試generateConfig配置文件",()=>{   
    expect(generateConfig()).toMatchSnapshot({
        date:expect.any(Date)
    });

})

在toMatchSnapshot()匹配器中傳入一個(gè)對(duì)象參數(shù),表示,date字段只要是Date類型即可,不需要完全相等。

行內(nèi)snapshot

需要安裝prettier模塊
npm install prettier@1.18.2 --save

import { generateConfig } from "./snap.js";
test("測(cè)試generateConfig配置文件", () => {
  expect(generateConfig()).toMatchInlineSnapshot(
    {
      date: expect.any(Date)
    }
  );
});

使用toMatchInlineSnapshot()匹配器,執(zhí)行測(cè)試命令之后,會(huì)將生成的快照自動(dòng)在toMatchInlineSnapshot()中的第二個(gè)參數(shù)展示。

test("測(cè)試generateConfig配置文件", () => {
  expect(generateConfig()).toMatchInlineSnapshot(
    {
      date: expect.any(Date)
    },
    `
    Object {
      "age": 40,
      "couple": true,
      "date": Any<Date>,
      "name": "lisa",
      "sex": "female",
    }
  `
  );
});

行內(nèi)快照(toMatchInlineSnapshot)與普通快照(toMatchSnapshot)的區(qū)別就是行內(nèi)快照生成快照文件存放在測(cè)試文件里,而普通快照是將快照文件生成一個(gè)新的文件夾存放。

命令行s是跳過當(dāng)前錯(cuò)誤提示,直接到下一個(gè)提示。

mock深入學(xué)習(xí)

在之前學(xué)習(xí)過異步測(cè)試可以通過模擬返回值進(jìn)行測(cè)試

// deepmock.js
import axios from 'axios';
export const getData = () => {
    return axios.get('http://www.dell-lee.com/react/api/demo.json')
}
// deepmock.test.js
import axios from 'axios';
jest.mock('axios');
test('getData',() => {
    axios.get.mockResolvedValue({data:"123"})
    return getData().then((data)=>{
        expect(data.data).toEqual('123')
    })
})

現(xiàn)在可以模擬一個(gè)專門用來模擬異步請(qǐng)求方法的文件:
在根目錄下創(chuàng)建一個(gè)文件夾__mocks__,里面寫一個(gè)和需要測(cè)試的文件完全一樣的名稱比如 deepmock.js里面可以模擬異步請(qǐng)求

// __mocks__/deepmock.js
export const getData = () => {
    return new Promise((resolved,rejected)=>{
        resolved("123")
    })
}

在deepmock.test.js中這么測(cè)試

jest.mock('./deepmock.js');
import { getData } from './deepmock.js';
test('getData',() => {
    return getData().then((data)=>{
        expect(data).toEqual('123')
    })
})

第一步模擬文件,第二步導(dǎo)入文件,此時(shí)導(dǎo)入的deepmock.js文件是__mocks__/deepmock.js文件,然后進(jìn)行測(cè)試。

如果將jest.config.jsautomock屬性設(shè)置為true,那么相當(dāng)于默認(rèn)進(jìn)行mock,可以不用寫jest.mock('./deepmock.js').

如果deepmock.js中有同步代碼,不需要放在__mocks__文件夾中的deepmock.js中時(shí),可以使用const { getNum } = jest.requireActual('./deepmock.js')來實(shí)現(xiàn)引用源文件中的getNum,而不使用模擬文件中的getNum.

比如:

//deepmock.js
import axios from 'axios';
export const getData = () => {
    return axios.get('http://www.dell-lee.com/react/api/demo.json')
}

export const getNum = () => {
    return 456
}
//__mocks__/deepmock.js
export const getData = () => {
    return new Promise((resolved,rejected)=>{
        resolved("123")
    })
}
//deepmock.test.js
jest.mock('./deepmock.js');
import { getData } from './deepmock.js';
const { getNum } = jest.requireActual('./deepmock.js');
test('getData',() => {
    return getData().then((data)=>{
        expect(data).toEqual('123')
    })
})
test('getNum',() => {
    expect(getNum()).toEqual(456)
})j

test文件中異步方法getData引用的是__mocks__/deepmock.js中的方法,而同步方法getNum引用的是根目錄下deepmock.js中的方法。

小總結(jié):
jest.mock("文件路徑"):模擬該文件中的方法。對(duì)應(yīng)在__mocks__/文件路徑下。
jest.unmock("文件路徑"):可以取消模擬文件。
requireActual:表示可以引用真實(shí)文件中的方法,而非模擬文件中的。

jest中的timer測(cè)試

根據(jù)之前學(xué)習(xí)可以測(cè)試延時(shí)代碼如下:

//timer.js
export const timer = function (fn){
    setTimeout(()=>{
        fn()
    },3000)
}
//timer.test.js
import {timer} from './timer.js';
test("timer", (done) => {
    timer(()=>{
        expect(1).toBe(1);
        done();
    })
})

這樣表示等3s之后會(huì)執(zhí)行一部中的測(cè)試語(yǔ)句,問題來了,如果延時(shí)為很長(zhǎng)時(shí)間的話,使用等待時(shí)間這種機(jī)制不是很好,那么就需要另外一種方法:使用假的定時(shí)器。

方法:jest.useFakeTimers()

//timer.test.js
jest.useFakeTimers();
test("test timers",()=>{
    const fn = jest.fn();
    timer(fn);
    jest.runAllTimers();
    expect(fn).toHaveBeenCalledTimes(1)
})

使用jest.useFakeTimers(),表示開始使用假的定時(shí)器,之后配合jest.runAllTimers()表示立即運(yùn)行玩所有的定時(shí)器。再配合toHaveBeenCalledTimes(1)匹配器,表示fn方法被調(diào)用了幾次,來進(jìn)行測(cè)試。注意:fn必須是一個(gè)mock函數(shù),否則會(huì)報(bào)錯(cuò)。

假如timer.js是這樣

//timer.js
export const timer = function (fn){
    setTimeout(()=>{
        fn()
        setTimeout(()=>{
            fn()
        },3000)
    },3000)
}

jest.runAllTimers()表示運(yùn)行所有的timer,假如只想檢測(cè)外層的setTimeout()那么這個(gè)方法是不行的,可以使用jest.runOnlyPendingTimers()表示只會(huì)運(yùn)行處于當(dāng)前運(yùn)行處于隊(duì)列中即將運(yùn)行的timer,而不會(huì)運(yùn)行那些還沒有被創(chuàng)建的timer。代碼如下:

jest.useFakeTimers();
test("test timers",()=>{
    const fn = jest.fn();
    timer(fn);
    jest.runOnlyPendingTimers()
    expect(fn).toHaveBeenCalledTimes(1)
})

小總結(jié):
runAllTimers():表示運(yùn)行所有的timer
runOnlyPendingTimers():表示只運(yùn)行當(dāng)前隊(duì)列中的timer,而不管還未創(chuàng)建的。

更好的api:jest.advanceTimersByTime(3000)表示讓時(shí)間快進(jìn)3s。

//timer.test.js
import {timer} from './timer.js';
jest.useFakeTimers();
test("test timers",()=>{
    const fn = jest.fn();
    timer(fn);
    jest.advanceTimersByTime(3000)
    expect(fn).toHaveBeenCalledTimes(1)
})

很明顯快進(jìn)3s之后,回調(diào)函數(shù)fn只執(zhí)行了一次,因此expect(fn).toHaveBeenCalledTimes(1)測(cè)試會(huì)通過。
如果將修改一下參數(shù)jest.advanceTimersByTime(6000),那么回調(diào)fn被執(zhí)行了兩次,因此想要通過測(cè)試必須修改斷言expect(fn).toHaveBeenCalledTimes(2)

//timer.test.js
import {timer} from './timer.js';
jest.useFakeTimers();
test("test timers",()=>{
    const fn = jest.fn();
    timer(fn);
    jest.advanceTimersByTime(6000)
    expect(fn).toHaveBeenCalledTimes(2)
})

或者

test("test timers",()=>{
    const fn = jest.fn();
    timer(fn);
    jest.advanceTimersByTime(3000)
    expect(fn).toHaveBeenCalledTimes(1)
    jest.advanceTimersByTime(3000)
    expect(fn).toHaveBeenCalledTimes(2)
})

表示快進(jìn)3s后fn被調(diào)用1次,再快進(jìn)3s后fn被調(diào)用2次。那就存在一個(gè)問題,每一次快進(jìn)是在前一次快進(jìn)基礎(chǔ)上進(jìn)行調(diào)用的有可能會(huì)有沖突,那么解決辦法就是在鉤子函數(shù)中進(jìn)行處理。

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

test("test timers",()=>{
    const fn = jest.fn();
    timer(fn);
    jest.advanceTimersByTime(3000)
    expect(fn).toHaveBeenCalledTimes(1)
    jest.advanceTimersByTime(3000)
    expect(fn).toHaveBeenCalledTimes(2)
})

test("test1 timers",()=>{
    const fn = jest.fn();
    timer(fn);
    jest.advanceTimersByTime(3000)
    expect(fn).toHaveBeenCalledTimes(1)
    jest.advanceTimersByTime(3000)
    expect(fn).toHaveBeenCalledTimes(2)
})

每次在進(jìn)入測(cè)試之前都重新jest.useFakeTimers()即可。

ES6中類的測(cè)試

單元測(cè)試:表示只僅僅對(duì)單一一個(gè)文件或方法進(jìn)行測(cè)試,從而忽略掉該文件中引用的其他內(nèi)容,如果其他內(nèi)容比較耗費(fèi)性能,那么在進(jìn)行單元測(cè)試的時(shí)候進(jìn)行mock簡(jiǎn)化引用。
舉例:在一個(gè)方法文件中引用了一個(gè)類。然后對(duì)該方法進(jìn)行單元測(cè)試。

//util.js (ES6類)
class Util {
    a(){
        //...邏輯復(fù)雜,耗費(fèi)性能
    }
    b(){
        //...邏輯復(fù)雜,耗費(fèi)性能
    }
}
export default Util

Util類中有兩個(gè)方法,都很耗性能,邏輯也很復(fù)雜。

//demoUtil.js
import Util from './util.js';

export const demoFunction = () => {
    let util = new Util();
    util.a();
    util.b();
}

在demoUtil文件中引用了這個(gè)Util類,并且使用了。

//demoUtil.test.js
jest.mock('./util.js');
/*
    Util Util.a Util.b jest.fn()
*/
import { demoFunction } from "./demoUtil.js";
import Util from './util.js';

test('測(cè)試demoFunction',() => {
    demoFunction();

    expect(Util).toHaveBeenCalled();
    // expect()
    expect(Util.mock.instances[0].a).toHaveBeenCalled();
    expect(Util.mock.instances[0].b).toHaveBeenCalled();
    console.log(Util.mock);
})

demoUtil.test.js文件中對(duì)demoFunction進(jìn)行測(cè)試,此時(shí)由于Util中類邏輯復(fù)雜耗費(fèi)性能,那么我們需要采取mock對(duì)其進(jìn)行模擬,只需要判斷在demoFunction方法中Util類,以及它的實(shí)例化方法調(diào)用了沒有即可。

這段代碼流程解釋一下就是:
jest.mock('./util.js'):當(dāng)jest.mock()中的參數(shù)檢測(cè)到是一個(gè)類時(shí),那么就會(huì)直接默認(rèn)將類,以及里面的方法轉(zhuǎn)換成mock函數(shù),例如:Util ,Util.a,Util.b都將會(huì)轉(zhuǎn)換成jest.fn();
引入Util,此時(shí)的Util已經(jīng)轉(zhuǎn)換成了jest.fn(),那么此時(shí)就可以采用Util.mock下的一些屬性進(jìn)行測(cè)試?yán)病?/p>

還有一種辦法就是,直接在__mocks__文件夾中自己去模擬實(shí)現(xiàn)一下jest.mock()方法的內(nèi)部實(shí)現(xiàn)。

//__mocks__/util.js
const Util = jest.fn(()=>{
    console.log("Util")
});
Util.prototype.a = jest.fn(()=>{
    console.log("a")
});
Util.prototype.b = jest.fn(()=>{
    console.log("b")
});
export default Util;

這樣就相當(dāng)于把jest.mock("./util"),自動(dòng)轉(zhuǎn)換過程手寫了一遍,這樣做比較優(yōu)雅,而且還可以對(duì)方法進(jìn)行拓展,寫一些邏輯。

還可以直接在demoUtil.test.js中直接寫

jest.mock('./util.js',()=>{
    const Util = jest.fn(()=>{
        console.log("Util")
    });
    Util.prototype.a = jest.fn(()=>{
        console.log("a")
    });
    Util.prototype.b = jest.fn(()=>{
        console.log("b")
    });
    return Util;
});
import { demoFunction } from "./demoUtil.js";

import Util from './util.js';

test('測(cè)試demoFunction',() => {
    demoFunction();

    expect(Util).toHaveBeenCalled();
    // expect()
    expect(Util.mock.instances[0].a).toHaveBeenCalled();
    expect(Util.mock.instances[0].b).toHaveBeenCalled();
    console.log(Util.mock);
})

在jest.mock()第二個(gè)參數(shù)中可以放剛才自動(dòng)轉(zhuǎn)換的邏輯。

單元測(cè)試就是指只對(duì)我自身做一些測(cè)試,集成測(cè)試是指對(duì)我自身以及自身其他依賴一起做測(cè)試。

jest中對(duì)dom節(jié)點(diǎn)測(cè)試

node環(huán)境中是沒有dom的,jest在node環(huán)境下自己模擬了一套dom的api,jsDom。
需要對(duì)dom操作為了方便,安裝jquery依賴。
看下面例子:

//dom.js
import $ from "jquery";
export const addDivToBody = () => {
    $('body').append("<div/>")
}
//dom.test.js
import {addDivToBody} from './dom';
import $ from 'jquery';
test("addDivToBody",() => {
    addDivToBody();
    addDivToBody();
    expect($('body').find("div").length).toBe(2)
})  

練習(xí)源碼:https://github.com/Mstian/jest-test

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容