前端自動化測試實踐03—jest異步處理&mock
文章目錄
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)
})