Jest前端自動化測試入門

薛大哈發表於2020-04-04

前言

近幾年,前端發展速度很快,這也意味著對前端工程化的要求會越來越高,而前端自動化測試作為程式碼質量保證的一環,也是前端工程化的範疇,目前很多開源框架或庫都使用了前端自動化測試,例如Ant DesignElement UI等,所以我們有必要學習一下前端自動化測試。

Jest是facebook推出的一款測試框架,整合了Mocha,chai,jsdom,sinon等功能。所以其強大的功能還是值得我們學習的。

jest的簡單配置

jest是執行在node環境下的,不支援es6Es Module,但我們測試程式碼通常是執行在瀏覽器的,所以有必要使用Babel進行程式碼轉換,下面對專案進行初始化和jest的簡單配置:

執行

npm init
複製程式碼

進行專案初始化,並在package.json中配置測試的script命令:

"script": {
    "test": 'jset'
}
複製程式碼

再安裝jest、@babel/core、@babel/preset-env

npm i jest @babel/core @babel/preset-env -D
複製程式碼

安裝完成後,執行

npx jest --init
複製程式碼

進行jest配置初始化,初始化完成後,就是再目錄多出一個jest.config.js配置檔案了。

接著建立.babelrc進行babel配置:

// .babelrc
{
    "presets": [
        ["@babel/preset-env", {
            "target": {
                "node": "current"
            }
        }]
    ]
}
複製程式碼

經過這些配置後,我們就可以在jest的測試檔案中使用Es Module了。使用jest進行測試時,我們對模組的測試需要遵循一定的檔案命名,命名規則為:

moduleName.test.js
複製程式碼

例如需要對button.js檔案模組進行測試,我們為此建立的測試檔名為button.test.js

一、前端自動化測試的原理

Jest擁有眾多API,可以測試各種開發場景,其核心APIexpect()test(),每個測試用例都離不開這兩個核心功能。

test()函式主要用於描述一個測試用例,expect()函式是用於指示我們期望的結果。而expect()test()原理思路很巧妙,並不複雜,下面程式碼就是兩者的原理思路:

function expect (result) {
    return {
        toBe: function (actual) {
            if (result !== actual) {
                throw new Error(`
                預期值與實際值不相等,預期值為${actual},實際值為${result}`);
            }
        }
    }
}

function test (desc, callback) {
    try {
        callback();
        console.info(`${desc} 通過測試`);
    } catch(e) {
        console.info(`${desc} 沒有通過測試:${e}`);
    }
}
複製程式碼

在這段程式碼中expact(result) 將返回我們期望的結果,通常情況下我們只需要呼叫expact(result)就可以,括號中的可以是一個具有返回值的函式,也可以是表示式。後面的toBe 就是一個matcher,當Jest執行的時候它會記錄所有失敗的matcher的詳細資訊並且輸出給使用者,讓維護者清楚的知道failed的原因。

二、Jest的API

2.1、 Matchers匹配器

匹配器(Matchers)是Jest中非常重要的一個概念,它可以提供很多種方式來讓你去驗證你所測試的返回值,匹配器可以通俗理解為相等操作。

2.1.1、相等匹配Matchers

  • toBe()

toBe()是測試expect的期望值是否全等於結果值。toBe()相當於js中的===、Object.is()

test('測試具體值:1 + 2 = 3',() => {
  expect(1 + 2).toBe(3);
});
複製程式碼
  • toEqual()

相對於toBe()的完全相等,toEqual()匹配的是內容是否相等,一般用於測試物件或陣列的內容相等。

test('測試物件內容相等',() => {
  let a = { a: 1 };
  expect(a).toEqual({ a: 1 });
});
複製程式碼

2.1.2、判斷匹配Matchers

toBeTruthy()是測試expect的期望值是否為true
toBeFalsy()是測試expect的期望值是否為false
toBeUndefined()是測試expect的期望值是否為undefined
toBeNull()是測試expect的期望值是否為null

test('判斷匹配Matchers',() => {
  expect(true).toBeTruthy();
  expect(false).toBeFalsy();
  expect().toBeUndefined();
  expect(null).toBeNull();
});
複製程式碼

2.1.3、數字匹配Matchers

toBeGreaterThan()是測試expect的期望值是否大於傳入的引數;
toBeLessThan()是測試expect的期望值是否小於傳入的引數;
toBeGreaterThanOrEqual()是測試expect的期望值是否大於或等於傳入的引數;
toBeLessThanOrEqual()是測試expect的期望值是否否小於或等於傳入的引數;

test('判斷匹配Matchers',() => {
  expect(2).toBeGreaterThan(1);
  expect(2).toBeLessThan(3);
  expect(2).toBeGreaterThanOrEqual(2);
  expect(2).toBeLessThanOrEqual(2);
});
複製程式碼

2.1.4、其他常用匹配Matchers

toMatch()是測試expect的期望值是否符合傳入的正規表示式匹配規則;
toContain()是測試expect的期望值是否包含傳入的引數;
toThrow()是測試expect的期望值是否會丟擲特定的異常資訊;

test('其他常用匹配Matchers',() => {
  expect(/2/).toMatch(2);
  expect('Other Matchers').toContain('Matchers');
  expect(() => { throw new Error('error'); }).toThrow('error');
});
複製程式碼

jest的匹配器有很多,但是我們不必全部都記住,可以靈活運用各種匹配器達到類似的效果,比如toBe(true)類似於toBeTruthy

三、Jest測試非同步程式碼

我們在開發過程中,難免會進行資料請求等非同步操作,Jest也考慮到了這一點,現在我們以非同步請求資料為例,來說明如何使用Jest進行非同步程式碼測試:

執行

npm i axios --save
複製程式碼

3.1、回撥函式非同步型別測試

建立fetch.js

//fetch.js
import axios from 'axios';

//假設'https://juejin.im/editor'返回的data資料為{ success: true }
export const fetchData = (callback) => {
    aixos.get('https://juejin.im/editor').then(res => {
        callback(res.data);
    });
}
複製程式碼

接著建立fetch.test.js進行測試:

//fetch.test.js
import { fetchData } from './fetchData.js';

test('測試返回的結果為 { success: true }', (done) => {
    fetchData((data) => {
        expect(data).toEqual({ success: true });
        done();
    })
});
複製程式碼

3.2、返回Promise非同步型別測試

3.2.1、Promise請求成功的測試

建立fetch.js

//fetch.js
import axios from 'axios';

//假設'https://juejin.im/editor'返回的data資料為{ success: true }
export const fetchData = (callback) => {
   return aixos.get('https://juejin.im/editor');
}
複製程式碼

接著建立fetch.test.js進行測試:

//fetch.test.js
import { fetchData } from './fetchData.js';

test('測試返回的結果為 { success: true }', () => {
    return fetchData().then(res => {
        const { data } = res;
        expect(data).toEqual({ success: true });
    })
});
複製程式碼

3.2.2、Promise請求失敗的測試

建立fetch.js

//fetch.js
import axios from 'axios';

//假設'https://juejin.im/editor/xxxx'沒有返回資料,為404狀態
export const fetchData = (callback) => {
   return aixos.get('https://juejin.im/editor/xxxx');
}
複製程式碼

接著建立fetch.test.js進行測試:

//fetch.test.js
import { fetchData } from './fetchData.js';

test('測試返回的結果為 404', () => {
    expect.assertions(1);
    return fetchData().catch(e => {
        expect(e.toString().indexOf('404') > -1).toBe(true);
    })
});
複製程式碼

四、Jest的鉤子函式

Jest的鉤子函式類似於Vue的生命週期函式,會在在程式碼執行的特定時刻,自動執行一個函式。Jest中有4個核心的鉤子函式,分別為beforeAll、beforeEach、afterEach、afterAll,鉤子函式均接受回撥函式作為引數。

4.1、Jest的鉤子函式執行順序

顧名思義,Jest的4個核心鉤子函式執行順序依次為beforeAllbeforeEachafterEachafterAll

  • beforeAll:該鉤子函式會在所有測試用例執行之前執行,通常用於進行初始化。
  • beforeEach:該鉤子函式會在每個測試用例執行之前執行。
  • afterEach:該鉤子函式會在每個測試用例執行之後執行。
  • afterAll:該鉤子函式會在所有測試用例執行之後執行。

下面以hook.test.js為例來說明一下鉤子函式的執行順序:

//hook.test.js

beforeAll(() => {
    console.info('beforeAll 鉤子函式執行');
})

beforeEach(() => {
    console.info('beforeEach 鉤子函式執行');
})

afterEach(() => {
    console.info('afterEach 鉤子函式執行');
})

afterAll(() => {
    console.info('afterAll 鉤子函式執行');
})

test('測試Jest中的鉤子函式', () => {
    console.info('測試用例執行');
    expect(1).toBe(1);
});
複製程式碼

執行

npm run test
複製程式碼

我們會發現終端,列印出以下資訊:

'beforeAll 鉤子函式執行'
'beforeEach 鉤子函式執行'
'測試用例執行'
'afterEach 鉤子函式執行'
'afterAll 鉤子函式執行'
複製程式碼

這也恰恰印證了上面說明的鉤子函式的執行順序。

4.2、藉助鉤子函式,提高程式碼維護性

合理運用鉤子函式,可以使我們的測試程式碼更加易於維護,下面以Calculator.js為例來說明一下鉤子函式的運用:

建立Calculator.js

//Calculator.js
class Calculator {
    constructor() {
        this.number = 0
    }
    add() {
        this.number += 1;
    }
    minus() {
        this.number -= 1;
    }
}
複製程式碼

接著建立Calculator.test.js進行測試:

//Calculator.test.js
import Calculator from './Calculator.js';

let calculator = null;

//Jest推薦使用鉤子函式進行初始化
beforeAll(() => {
    calculator = new Calculator();
})

test('測試Calculator中的 add 方法', () => {
    calculator.add();
    expect(calculator.number).toBe(1);
});

test('測試Calculator中的 minus 方法', () => {
    //共用了同一個```calculator```例項,與上一個程式碼耦合
    calculator.minus();
    expect(calculator.number).toBe(0); 
});
複製程式碼

執行

npm run test
複製程式碼

我們會發現測試用例是順利通過的,不過Calculator.test.js中的程式碼寫法有一個問題:在每個測試用例中都共用了同一個calculator例項,導致每個測試函式互相耦合,不利於維護,為了解決這個問題我們可以藉助JestbeforeEach鉤子函式進行解耦。

修改Calculator.test.js

//Calculator.test.js
import Calculator from './Calculator.js';

let calculator = null;

beforeEach(() => {
    calculator = new Calculator();
})

test('測試Calculator中的 add 方法', () => {
    calculator.add();
    expect(calculator.number).toBe(1);
});

test('測試Calculator中的 minus 方法', () => {
    calculator.minus();
    expect(calculator.number).toBe(-1);
});
複製程式碼

執行

npm run test
複製程式碼

我們會發現測試用例是順利通過的,通過beforeEach鉤子函式,我們在每個測試用例執行之前,重新建立了calculator例項,這樣每個測試用例之間就沒有互相耦合,程式碼會更加容易維護。

五、藉助describe進行測試用例分組管理

我們在開發過程中都會遇到功能繁多的複雜模組,為此我們需要寫大量測試用例來測試這些模組,但如果單純為模組的每個功能寫測試用例而不加以分類的話,Jest測試檔案將十分混亂而難以維護,因此我們需要對模組功能進行分類,在藉助Jest的describe進行分組管理。

以上面的Calculator.js以及Calculator.test.js為例,來說明如何使用describe進行分組管理:

修改Calculator.js

//Calculator.js
class Calculator {
    constructor() {
        this.number = 0
    }
    add() {
        this.number += 1;
    }
    minus() {
        this.number -= 1;
    }
    multiply() {
        this.number *= 2;
    }
    divide() {
        this.number /= 10;
    }
}
複製程式碼

修改Calculator.test.js

//Calculator.test.js
import Calculator from './Calculator.js';

describe('測試Calculator模組所有功能',() => {
    let calculator = null;

    beforeEach(() => {
        calculator = new Calculator();
    });
    
    describe('測試Calculator中增加數量相關的方法',() => {
        test('測試Calculator中的 add 方法', () => {
            calculator.add();
            expect(calculator.number).toBe(1);
        });
        test('測試Calculator中的 multiply 方法', () => {
            calculator.multiply();
            expect(calculator.number).toBe(0);
        });
    });
    
    describe('測試Calculator中減少數量相關的方法',() => {
        test('測試Calculator中的 minus 方法', () => {
            calculator.minus();
            expect(calculator.number).toBe(-1);
        });
        test('測試Calculator中的 divide 方法', () => {
            calculator.divide();
            expect(calculator.number).toBe(0);
        });
    });
})
複製程式碼

執行

npm run test
複製程式碼

我們會發現終端,列印出以下層次分明、可讀性良好的測試資訊:

'測試Calculator模組所有功能'
    '測試Calculator中增加數量相關的方法''測試Calculator中的 add 方法''測試Calculator中的 multiply 方法'
    '測試Calculator中減少數量相關的方法''測試Calculator中的 minus 方法''測試Calculator中的 divide 方法'
複製程式碼

我們可以看到,對模組功能進行分類,以及藉助Jest的describe進行分組管理,我們測試程式碼的可維護性以及測試執行結果資訊可讀性都有顯著的提高了。

六、describe中的鉤子函式執行規則

每個describe的回撥函式都有各自的作用域,都可以使用Jest的4個核心鉤子函式,而describe中的鉤子函式都可以作用於它回撥函式中的所有測試用例。

面對像上面Calculator.test.js檔案中,describe回撥函式巢狀describe的場景,每個describe中的鉤子函式執行順序會有點特別。

以下對Calculator.test.js進行修改,來說明describe回撥函式巢狀describe場景的鉤子函式執行順序:

修改Calculator.test.js

//Calculator.test.js
import Calculator from './Calculator.js';

describe('測試Calculator模組所有功能',() => {
    let calculator = null;
    
    beforeAll(() => {
        console.info('beforeAll: 父級 beforeAll 執行');
    });
    
    beforeEach(() => {
        calculator = new Calculator();
        console.info('beforeEach: 父級 beforeEach 執行');
    });
    
    afterEach(() => {
        console.info('afterEach: 父級 afterEach 執行');
    });
    
    describe('測試Calculator中增加數量相關的方法',() => {
        beforeAll(() => {
            console.info('beforeAll: 第一個子級 beforeAll 執行');
        });
        
        test('測試Calculator中的 add 方法', () => {
            console.info('測試Calculator中的 add 方法');
            calculator.add();
            expect(calculator.number).toBe(1);
        });
    });
    
    describe('測試Calculator中減少數量相關的方法',() => {
        beforeEach(() => {
            console.info('beforeEach: 第二個子級 beforeEach 執行');
        });
        
        test('測試Calculator中的 minus 方法', () => {
            console.info('測試Calculator中的 minus 方法');
            calculator.minus();
            expect(calculator.number).toBe(-1);
        });
    });
})
複製程式碼

執行

npm run test
複製程式碼

我們會發現終端,列印出以下資訊:

'beforeAll: 父級 beforeAll 執行'
'beforeAll: 第一個子級 beforeAll 執行'
'beforeEach: 父級 beforeEach 執行'
'測試Calculator中的 add 方法'
'afterEach: 父級 afterEach 執行'
'beforeEach: 父級 beforeEach 執行'
'beforeEach: 第二個子級 beforeEach 執行'
'測試Calculator中的 minus 方法'
'afterEach: 父級 afterEach 執行'
複製程式碼

從中,我們可以發現在describe回撥函式巢狀describe的場景下,describe中的鉤子函式都可以作用於它回撥函式中的所有測試用例,並且每個測試用例在describe作用域中執行時,相同型別鉤子函式的執行順序是先從父級到子級從外部到內部的。

七、Jest中的Mock

從測試的角度來說我只關心測試的方法的內部邏輯,並不關注與當前方法本身依賴的實現,所以,我們在測試某些依賴了外部的一些介面的實現的方法時,通常會進行Mock實現依賴介面的返回,只測試方法的內部邏輯而規避外部的依賴,基於這個思想,jest中提供了強大的Mock功能,方便開發者進行mock操作。

7.1、使用jest.fn()函式,捕獲函式的呼叫

開發過程中,一個功能函式傳入回撥函式作為引數的場景很常見,如果想測試這類功能函式,我們就需要藉助Mock函式,捕獲函式的呼叫。

callback.js以及callback.test.js為例,來說明Mock函式:

建立callback.js

//callback.js
export const testCallback = (callback) => {
    callback();
}
複製程式碼

建立callback.test.js

//callback.test.js
import { testCallback } from './callback.js';

test('測試 testCallback 方法', () => {
    const fn = jest.fn();
    testCallback(fn);
    expect(fn).toBeCalled();
})
複製程式碼

執行

npm run test
複製程式碼

我們會發現測試用例順利通過,上面程式碼通過jest.fn()mock出一個fn函式作為testCallback的回撥函式,在使用toBeCalled來捕獲fn的呼叫情況來驗證測試結果。值得注意的是,只有jest.fn()mock出的函式才可以被toBeCalled捕獲。

7.2、Mock函式的.mock屬性

jest.fn()mock出的函式會有一個.mock屬性,藉助.mock屬性我們可以多種方式去測試功能模組。

以上面的callback.js以及callback.test.js為例,來說明Mock函式:

修改callback.test.js

//callback.test.js
import { testCallback } from './callback.js';

test('測試 testCallback 方法', () => {
    const fn = jest.fn();
    testCallback(fn);
    testCallback(fn);
    console.info(fn.mock);
    expect(fn).toBeCalled();
})
複製程式碼

執行後,我們會發現終端,列印出以下.mock屬性資訊:

{
    calls: [ [], [] ],
    instances: [ undefined, undefined ],
    invocationCallOrder: [ 1, 2 ],
    results: [
        { type: 'return', value: undefined },
        { type: 'return', value: undefined }
    ]
}
複製程式碼

7.2.1、mock物件的calls屬性

fn.mock中的calls屬性是一個二維陣列,二維陣列中的陣列項表示傳入jest.fn()mock出的函式的實參,類似於arguments屬性。

從上面callback.test.js可以看到,fn是由jest.fn()mock出的函式,fn傳入testCallback中被呼叫了兩次並且fn並沒有接受引數,所以,calls二維陣列length為2,有2個空陣列項。

我們可以修改callback.js,給callback傳入引數,看一下此時calls的變化:

//callback.js
export const testCallback = (callback) => {
    callback(1, 2, 3);
}
複製程式碼

執行callback.test.js後,我們會發現終端,列印出以下.mock屬性資訊:

{
    calls: [ [1, 2, 3], [1, 2, 3] ],
    instances: [ undefined, undefined ],
    invocationCallOrder: [ 1, 2 ],
    results: [
        { type: 'return', value: undefined },
        { type: 'return', value: undefined }
    ]
}
複製程式碼

我們可以發現calls變為了[ [1, 2, 3], [1, 2, 3] ],也就是說calls的陣列項的確是表示傳入jest.fn()mock出的函式的實參。

7.2.2、mock物件的invocationCallOrder屬性

fn.mock中的invocationCallOrder屬性是一個陣列,陣列指示jest.fn()mock出的函式執行的順序。

從上面callback.test.js可以看到,fn是由jest.fn()mock出的函式,fn傳入testCallback中被呼叫了兩次,所以,invocationCallOrder陣列的length為2。

7.2.3、mock物件的results屬性

fn.mock中的results屬性是一個陣列,陣列中的物件指示jest.fn()mock出的函式每次執行的返回值,返回值由value屬性表示。

從上面callback.test.js可以看到,fn是由jest.fn()mock出的函式,fn傳入testCallback中被呼叫了兩次並且fn並沒有返回值,所以,results陣列的length為2,有2個物件,物件中的value屬性均為undefined

我們可以修改callback.test.js,使fn有返回值,看一下此時results的變化:

//callback.test.js
import { testCallback } from './callback.js';

test('測試 testCallback 方法', () => {
    const fn = jest.fn(() => {
        return 123;
    });
    testCallback(fn);
    testCallback(fn);
    console.info(fn.mock);
    expect(fn).toBeCalled();
})
複製程式碼

執行callback.test.js後,我們會發現終端,列印出以下.mock屬性資訊:

{
    calls: [ [1, 2, 3], [1, 2, 3] ],
    instances: [ undefined, undefined ],
    invocationCallOrder: [ 1, 2 ],
    results: [
        { type: 'return', value: 123 },
        { type: 'return', value: 123 }
    ]
}
複製程式碼

我們可以發現results中的物件的value變為了fn的返回值123,也就是說results陣列中的物件的確指示jest.fn()mock出的函式每次執行的返回值,返回值由value屬性表示。

7.2.4、mock物件的instances屬性

fn.mock中的instances屬性是一個陣列,陣列指示jest.fn()mock函式的this指向。當mock函式當作普通函式呼叫時,this指向undefined;當mock函式當作建構函式被new例項化時,this指向mockConstructor{}

從上面callback.test.js可以看到,fn是由jest.fn()mock出的函式,fn作為回撥函式傳入testCallback中被呼叫了兩次,所以,fnthis指向undefinedinstances陣列的length為2,陣列項均為undefined

我們可以修改callback.js,使fn以建構函式形式被呼叫,看一下此時instances的變化。

修改callback.js

//callback.js
export const testCallback = (callback) => {
    callback();
};

export const testInstances = (callback) => {
    new callback();
}
複製程式碼

再修改callback.test.js

//callback.test.js
import { testCallback, testInstances } from './callback.js';

test('測試 testInstances 方法', () => {
    const fn = jest.fn();
    testInstances(fn);
    testInstances(fn);
    console.info(fn.mock);
    expect(fn).toBeCalled();
})
複製程式碼

執行callback.test.js後,我們會發現終端,列印出以下.mock屬性資訊:

{
    calls: [ [], [] ],
    instances: [ mockConstructor{}, mockConstructor{} ],
    invocationCallOrder: [ 1, 2 ],
    results: [
        { type: 'return', value: undefined },
        { type: 'return', value: undefined }
    ]
}
複製程式碼

我們可以發現instances中變為了[ mockConstructor{}, mockConstructor{} ],也就是說instances的確指示了jest.fn()mock函式的this指向。當mock函式當作普通函式呼叫時,this指向undefined;當mock函式當作建構函式被new例項化時,this指向mockConstructor{}

7.3、使用Mock函式,改變內部函式的實現

在測試功能模組時,有時候我們想省略不想功能模組的某一執行步驟,不完全按照功能模組的程式碼邏輯執行,如果我們去改變功能模組的原始碼,那是不可取的,為此,我們可以藉助Mock函式,改變內部函式的實現。

假設現在我們

建立mockFetch.js:

//mockFetch.js
import axios from 'axios';

export const fetchData = () => {
    return axios.get('https://juejin.im/editor').then(res => res.data);
}
複製程式碼

建立mockFetch.test.js:

//mockFetch.test.js
import axios from 'axios';
import { fetchData } from './mockFetch.js';

jest.mock(axios);

test('測試 Mock 函式,改變內部函式的實現', () => {
    axios.get.mockResolvedValue({ data: { success: true } });
    return fetchData().then(data => {
        expect(data).toEqual({ success: true });
    })
});
複製程式碼

執行

npm run test
複製程式碼

我們會發現測試用例順利通過了。在上面程式碼中,我們使用jest.mock()axios進行包裝處理並且使用mockResolvedValue定義了axios.get請求的資料為{ data: { success: true } },這樣就使得fetchData不會真實地非同步請求'https://juejin.im/editor'介面,而是同步地以{ data: { success: true }為資料請求結果。

藉助jest.mock()mockResolvedValue,我們就可以改變axios模組本身的函式實現,使得該模組不會完全按照自身程式碼邏輯來執行。

7.4、建立__mocks__資料夾,改變內部函式的實現

除了像7.3小節那樣使用jest.mock函式,來改變內部函式的實現外,我們還可以藉助建立__mocks__檔案,改變內部函式的實現。

假設現在我們繼續以mockFetch.js為例,對7.3小節的檔案進行修改:

mockFetch.js同級目錄下建立__mocks__資料夾,在該資料夾下建立mockFetch.js,如下:

//__mocks__資料夾中的mockFetch.js

export const fetchData = () => {
   console.log('__mocks__資料夾中的mockFetch.js 的fetchData執行');
   const data = { success: true };
   return Promise.resolve(data);
}
複製程式碼

修改mockFetch.js:

//mockFetch.js
import axios from 'axios';

export const fetchData = () => {
   console.log('mockFetch.js 的fetchData執行');
   return axios.get('https://juejin.im/editor').then(res => res.data);
}
複製程式碼

修改mockFetch.test.js:

//mockFetch.test.js
jest.mock('./mockFetch.js');
import { fetchData } from './mockFetch.js';

test('測試建立__mocks__資料夾,改變內部函式的實現', () => {
    return fetchData().then(data => {
        expect(data).toEqual({ success: true });
    })
});
複製程式碼

執行

npm run test
複製程式碼

我們會發現測試用例順利通過了,檢視控制檯可以看到以下的資訊:

'__mocks__資料夾中的mockFetch.js 的fetchData執行'
複製程式碼

這樣就說明了,測試用例執行的是__mocks__資料夾中的mockFetch.jsfetchData,之所以這樣,是因為在上面程式碼中,我們使用jest.mock('./mockFetch.js')./mockFetch.js進行mock處理,使得import { fetchData } from './mockFetch.js';中的./mockFetch.js為我們建立的__mocks__資料夾中的mockFetch.js,通過這樣就藉助建立__mocks__檔案,改變檔案的引用來改變內部函式的實現了。

八、Snapshot快照測試

我們在開發元件的過程中,往往需要為元件建立一份預設Props配置,在元件升級迭代時,我們有可能會增加或修改預設Props的配置,這樣導致我們可能會修改錯誤某些配置而我們沒有感知到,造成修改引入bug,為了避免這種情況,我們可以藉助Snapshot生成檔案快照歷史記錄,以便在每次修改時進行修改提示,使開發者感知修改。

建立generateProps.js進行Snapshot說明:

//generateProps.js
export const generateProps = () => {
    return {
        name: 'jest',
        time: '2020',
        onChange: () => {}
    }
}
複製程式碼

建立generateProps.test.js:

//generateProps.test.js
import { generateProps } from './generateProps.js'

test('Snapshot快照測試', () => {
    expect(generateProps()).toMatchSnapshot();
})
複製程式碼

首次執行

npm run test
複製程式碼

我們可以發現測試用例順利通過,並且會在當前檔案目錄中新增一個_snapshot_資料夾,_snapshot_資料夾就是generateProps()返回值的一份快照,記錄了當次generateProps()執行的結果,以便作為下次變更的參照。

修改generateProps.js:

//generateProps.js
export const generateProps = () => {
    return {
        name: 'jest',
        time: '2020',
        desc: '測試',
        onChange: () => {}
    }
}
複製程式碼

執行

npm run test
複製程式碼

我們會發現終端控制檯報錯:

1 snapshot failed
複製程式碼

這是因為修改後的檔案內容與快照不匹配,如果我們確定需要更新修改,那麼我們就要在控制檯終端進入Jest命令列模式,輸入u來確定更新快照。

九、測試Timer定時器

開發過程中的setTimeoutsetInterval()等定時器都是非同步的,如果想測試它們,我們可以參照 三、Jest測試非同步程式碼

不過如果定時器設定的時間過於大的場景下,需要我們去等待定時器的觸發才可以知道測試結果的話,這明顯是不合理的,我們沒必要浪費時間去等待,Jest也清楚這一點,所以,我們可以藉助jest.useFakeTimers()以及jest.advanceTimersByTime()立即觸發定時器,提高開發效率。

下面建立timer.js,來說明如何測試定時器,:

// timer.js
export const timer = (callback) => {
    setTimeout(() => {
        callback()
    }, 2000) ;
}
複製程式碼

建立timer.test.js:

// timer.test.js
import { timer } from './timer.js';

jest.useFakeTimers();

test('測試定時器', () => {
    const fn = jest.fn();
    timer(fn);
    jest.advanceTimersByTime(2000);
    expect(fn).toHaveBeenCalledTimes(1);
});
複製程式碼

執行

npm run test
複製程式碼

我們可以發現測試用例不用等待定時器設定的時間,就順利通過了。在這裡,我們使用jest.useFakeTimers()來啟動假的定時器,然後藉助jest.advanceTimersByTime()來快進2000秒,所以相當於定時器已經成功觸發1次了,符合測試期望值。

十、對DOM節點的測試

Jest是執行在node的環境的,理論上node並沒有DOM這個概念,Jest為了方便開發者可以測試DOM節點操作,Jest自己在node的環境下模擬了一套API,我們可以稱它為JSDOM。藉助JSDOM特性,我們也可以在使用Jest來測試DOM節點。

下面以dom.js以及jQuery為例,來說明如何使用Jest來測試DOM節點:

執行

npm i jquery
複製程式碼

建立dom.js

//dom.js
import $ from 'jquery';

export const createDiv = () => {
    $('body').append('<div/>')
}
複製程式碼

建立dom.test.js

//dom.test.js
import { createDiv }from './dom.js';

test(' 測試 DOM 節點 ', () => {
    createDiv();
    let length = $('body').find('div').length;
    expect(length).toBe(1);
});
複製程式碼

執行

npm run test
複製程式碼

我們可以發現測試用例順利通過,也就是說,Jest支援測試DOM節點。

相關文章