jest-vue前端自動化測試實踐03—非同步處理&mock
- 2019 年 11 月 1 日
- 筆記
jest-vue前端自動化測試實踐03—非同步處理&mock
[toc]
Write By CS逍遙劍仙 我的主頁: www.csxiaoyao.com GitHub: github.com/csxiaoyaojianxian Email: [email protected]
本節程式碼地址 https://github.com/csxiaoyaojianxian/JavaScriptStudy 下的自動化測試目錄
1. async 非同步請求處理
一般項目程式碼中會有不少非同步 ajax 請求,例如測試下面 async.js
中的程式碼
import axios from 'axios'; // 傳入 callback 函數進行處理 export const fetchData1 = (fn) => { axios.get('http://www.csxiaoyao.com/api/data.json').then((response) => { fn(response.data); }) } // 返回 promise 交給後續程式處理 export const fetchData2 = () => { return axios.get('http://www.csxiaoyao.com/api/data.json') }
新建測試用例文件 async.test.js
進行測試
import {fetchData1, fetchData2} from './async'; ...
【1】callback 中處理,需要手動結束 done,否則可能走不到 callback
test('fetchData1 返回結果為 { success: true }', (done) => { fetchData1((data) => { expect(data).toEqual({ success: true }); // 如果不寫 done(),當介面404會導致用例不執行 done(); }) })
【2】返回 promise
處理成功,需要指定返回 expect 數量,否則可能直接走失敗分支跳過
test('fetchData2 返回結果為 { success: true }', () => { // 指定執行返回的 expect 數量 expect.assertions(1); return fetchData2().then((response) => { expect(response.data).toEqual({ success: true }); }) })
處理失敗,需要指定返回 expect 數量,否則可能直接走成功分支跳過
test('fetchData2 返回結果為 404', () => { // 當介面不為404,則不會走catch expect.assertions(1); return fetchData2().catch((e) => { expect(e.toString().indexOf('404') > 1).toBe(true); }) })
【3】promise – resolves / rejects 處理方式
promise – resolves
test('fetchData2 返回結果為 { success: true }', () => { return expect(fetchData2()).resolves.toMatchObject({ data: { success: true } }); })
promise-rejects
test('fetchData2 返回結果為 404', () => { return expect(fetchData2()).rejects.toThrow(); })
【4】promise-async|await 處理方式
成功處理方式1
test('fetchData2 返回結果為 { success: true }', async () => { await expect(fetchData2()).resolves.toMatchObject({ data: { success: true } }); })
成功處理方式2
test('fetchData2 返回結果為 { success: true }', async () => { const response = await fetchData2(); expect(response.data).toEqual({ success: true }); })
失敗處理方式1
test('fetchData2 返回結果為 404', async () => { await expect(fetchData2()).rejects.toThrow(); })
失敗處理方式2
test('fetchData2 返回結果為 404', async () => { expect.assertions(1); try { await fetchData2(); } catch (e) { expect(e.toString().indexOf('404') > -1).toBe(true); } })
2. mock – ajax 模擬 ajax 請求
介面的正確性一般由後端自動化測試保證,前端自動化測試,一般需要 mock 觸發的 ajax 請求,例如測試 mock.js
中介面調用
export const getData = () => { return axios.get('/api').then(res => res.data) }
測試用例,jest.mock('axios')
模擬 axios 請求
import { getData } from './mock' import axios from 'axios' // jest 模擬 axios 請求 jest.mock('axios') test('測試 axios getData', async () => { // 模擬函數的返回,getData 不會真正發起 axios 請求 axios.get.mockResolvedValueOnce({ data: 'hello' }) axios.get.mockResolvedValueOnce({ data: 'world' }) // axios.get.mockResolvedValue({ data: 'hello' }) await getData().then((data) => { expect(data).toBe('hello') }) await getData().then((data) => { expect(data).toBe('world') }) })
3. __mocks__ 文件替換 ajax
如果需要測試 mock.js
中 ajax 請求
export const fetchData = () => { return axios.get('/api').then(res => res.data) // '(function(){return 123})()' }
除了上述方法指定 mock 函數和返回結果,還可以使用 mock 文件替換對應方法,讓非同步變同步,需要在 __mocks__
文件夾下建立同名文件,如 __mocks__/mock.js
export const fetchData = () => { return new Promise ((resolved, reject) => { resolved('(function(){return 123})()') }) }
測試用例,對於在 mock.js
但不在 __mocks__/mock.js
中的方法則不會被覆蓋
import { fetchData } from './mock' jest.mock('./mock'); // jest.unmock('./08-mock2'); // 取消模擬 test('測試 fetchData', () => { return fetchData().then(data => { expect(eval(data)).toEqual(123); }) })
還可以設置自動 mock,jest.config.js
中打開 automock: true
,程式會自動在 mocks 文件夾下找同名文件,省去了手動調用 jest.mock('./mock');
4. mock – function 模擬函數調用
對於單元測試,無需關心外部傳入的函數的實現,使用 jest.fn
生成一個 mock 函數,可以捕獲函數的調用和返回結果,以及this和調用順序,例如測試 mock.js
export const runCallback = (callback) => { callback(123) }
測試用例
import { runCallback } from './mock' test('測試 callback', () => { // 【1】使用 jest 生成一個 mock 函數 func1,用來捕獲函數調用 const func1 = jest.fn() // 【2】模擬返回數據 // 1. mockReturnValue / mockReturnValueOnce // func1.mockReturnValue(10) func1.mockReturnValueOnce(456).mockReturnValueOnce(789) // 2. 回調函數 const func2 = jest.fn(() => { return 456 }) // 等價於 func2.mockImplementation(() => { return 456 }) // func2.mockImplementationOnce(() => { return this }) // func2.mockReturnThis // 【3】執行3次func1,1次func2 runCallback(func1) runCallback(func1) runCallback(func1) runCallback(func2) // 【4】斷言 // 被執行 expect(func1).toBeCalled() // 調用次數 expect(func1.mock.calls.length).toBe(3) // 傳入參數 expect(func1.mock.calls[0]).toEqual([123]) expect(func1).toBeCalledWith(123) // 返回結果 expect(func2.mock.results[0].value).toBe(456) // 【5】輸出mock,進行觀察 console.log(func1.mock) })
輸出的 mock 為
{ calls: [ [ 123 ], [ 123 ], [ 123 ] ], instances: [ undefined, undefined, undefined ], invocationCallOrder: [ 1, 2, 3 ], results: [ { type: 'return', value: 456 }, { type: 'return', value: 789 }, { type: 'return', value: undefined } ] }
5. mock – function 模擬 class 函數
對於單元測試,外部 class 的實現無需關心,使用 jest.fn
生成一個 mock 類,例如測試 mock.js
export const createObject = (classItem) => { new classItem() }
測試用例
import { createObject } from './mock' test('測試 createObject', () => { const func = jest.fn() createObject(func) console.log(func.mock) })
輸出結果為
{ calls: [ [] ], instances: [ mockConstructor {} ], invocationCallOrder: [ 1 ], results: [ { type: 'return', value: undefined } ] }
6. mock – class 模擬實例化 class
例如測試 func.js
,從外部引入了 Util 類,但單元測試不關心 Util 的實現
import Util from './es6-class' const demoFunction = (a, b) => { const util = new Util() util.a(a) util.b(b) } export default demoFunction
有三種方案進行模擬
【1】jest.mock 真實 class 文件
jest.mock('./es6-class')
jest.mock 如果發現是一個類,會自動把構造函數和方法變成 jest.fn() 以提升性能,相當於執行了
const Util = jest.fn() Util.a = jest.fn() Util.b = jest.fn()
【2】自定義 jest.mock 傳參
jest.mock('./es6-class', () => {const Util = jest.fn() ... })
【3】在 __mocks__
中編寫同名文件覆蓋
__mocks__
文件除了可以替換 ajax 請求,還能替換 class 等,編寫 __mocks__/es6-class.js
const Util = jest.fn(() => { console.log('constructor --') }) Util.prototype.a = jest.fn(() => { console.log('a --') }) Util.prototype.b = jest.fn() export default Util
編寫測試用例
import demoFunction from './func' import Util from './es6-class' test('測試 demo function', () => { demoFunction() expect(Util).toHaveBeenCalled() expect(Util.mock.instances[0].a).toHaveBeenCalled() expect(Util.mock.instances[0].b).toHaveBeenCalled() console.log(Util.mock) })
輸出 mock
{ calls: [ [] ], instances: [ Util { a: [Function], b: [Function] } ], invocationCallOrder: [ 1 ], results: [ { type: 'return', value: undefined } ] }
7. mock – timer 模擬定時器
例如測試 timer.js
export default (callback) => { setTimeout(() => { callback(); setTimeout(() => { callback(); }, 3000); }, 3000); }
如果直接使用 done,需要等定時器執行,要等待較長時間,影響測試效率
test('測試 timer', (done) => { timer(() => { expect(1).toBe(1) done() }) })
因此需要使用 useFakeTimers
/ runAllTimers
/ runOnlyPendingTimers
/ advanceTimersByTime
來縮短 timers 時間,對於本案例
【1】定時器立即執行
jest.runAllTimers() // 執行2次
【2】只運行隊列中的timer
jest.runOnlyPendingTimers() // 執行1次
【3】快進x
jest.advanceTimersByTime(3000) // 快進3s
import timer from './timer' // 各個用例之間定時器不影響 beforeEach(() => { jest.useFakeTimers() }) test('測試 timer', () => { const fn = jest.fn() timer(fn) jest.advanceTimersByTime(3000) // 快進3s expect(fn).toHaveBeenCalledTimes(1) jest.advanceTimersByTime(3000) // 再快進3s expect(fn).toHaveBeenCalledTimes(2) })
