網(wǎng)上有很多vue-test-utils + jest 的相關(guān)文章,大體上可以分為兩個流派:
- 配置搭建派
- 特殊問題派
通觀起來總覺得似乎對于真正想要實(shí)施單元測試的入門開發(fā)人員來說,缺了一環(huán):面對具體場景時,應(yīng)該如何進(jìn)行單元測試編碼。
這篇文章就根據(jù)不同場景,列出一些真實(shí)的代碼示例。希望能夠幫助到想要真正實(shí)施單元測試的團(tuán)隊。
mock 實(shí)例方法
場景舉例
submit方法內(nèi),一般會調(diào)用校驗(yàn)方法,依賴校驗(yàn)方法的返回 true || false 來判斷是否進(jìn)行實(shí)際提交邏輯的執(zhí)行。
但有時候需要測試的方法內(nèi)調(diào)用的其他方法很難構(gòu)造,比如校驗(yàn)方法需要傳遞若干參數(shù),而且所有參數(shù)必須合規(guī)才能繼續(xù)執(zhí)行提交的代碼邏輯。那么此時讓校驗(yàn)方法直接通過比較好。
當(dāng)然這引出另外一個話題,我們在進(jìn)行單元測試的時候是否應(yīng)該連帶引用方法一起測試?
我的看法是,既然是單元測試,那么引用方法不一定必須全部覆蓋到,原因如下:
在TDD過程中需測試的方法和其中的引用方法不一定是同一個人負(fù)責(zé)的
單元測試應(yīng)該僅為當(dāng)前測試方法的邏輯是否正確負(fù)責(zé),如提交方法需要考慮的是提交的各種成功或者失敗場景,而校驗(yàn)方法應(yīng)該自行通過自己的單元測試代碼來確保校驗(yàn)本身是否正確
因此在這種場景下,我們對于特別復(fù)雜的引用方法可以考慮進(jìn)行mock。
代碼如下
jest.spyOn(wrapper2.vm, 'validate').mockImplementation(() => {return true})
wrapper2.vm.handleSubmit() // 再直接調(diào)用提交方法的話,校驗(yàn)方法就會直接返回成功,以便我們直接對提交內(nèi)部的方法進(jìn)行測試
對于特定字段的斷言
場景舉例
有時候在邏輯代碼中一個方法的調(diào)用,其參數(shù)可能非常龐大,但是對于某種測試場景,我們只需要判斷參數(shù)中是否含有某幾個特定字段或者值。
比如我們調(diào)用一個方法,其代碼如下
function fn () {
...
let params = {
key1: value1,
key2: value2,
key3: value3,
...
keyn: valuen
}
...
request.get(params)
}
在進(jìn)行單元測試編寫中,我們需要校驗(yàn)的是某種場景下,params必須含有 keyJ = J 的參數(shù)值。此時,我們不需要對整個params對象進(jìn)行斷言
let params = {
key1: value1,
key2: value2,
key3: value3,
...
keyn: valuen
}
// 上面完全手動構(gòu)建了完整的參數(shù)對象,不要這樣做
expect(request.get).toBeCalledWith(params)
如果只關(guān)心一個大對象其中的某個字段的值,不要像上面這樣編寫測試代碼,請參考下面的測試代碼示例
expect(request.get).toBeCalledWith(expect.objectContaining({
key3: 42 // 假設(shè)我們構(gòu)建的測試場景只需要特別關(guān)注該場景下的key3 值必須為 42
}))
關(guān)于refs的處理
在業(yè)務(wù)代碼中含有調(diào)用當(dāng)前組件refs引用自組件,并調(diào)用他們方法的時候要怎么處理呢?
考慮如下場景
methods: {
save: () => {
this.$refs[name].validate((valid) => {
if (valid) {
......
}
})
}
}
我們應(yīng)該如何為save方法編寫測試代碼?
如果使用vue test utils的shallowMount方法,實(shí)際上不會在測試中實(shí)例化嵌套組件。比如上面這段代碼,這里引用到的refs指向的是Form組件。如果直接在測試中執(zhí)行wrapper.vm.save(),會報錯顯示調(diào)用了undefined 的 validate 方法。
這時候需要在shallowMount的時候通過stubs屬性來模擬加載一個嵌入的組件
const Form = {
render: jest.fn(),
methods: {
validate: (cb) => {cb(true)}
}
}
wrapper = shallowMount(indexTip, {
localVue,
store,
propsData: {},
stubs: {Form}
})
先聲明這個需要stub的Form組件。這個新聲明的組件只用來模擬測試中可能需要的行為,以避免引起undefined的異常,所以只需要聲明必要的方法即可。
- render方法直接聲明為jest.fn(),因?yàn)椴恍枰獙?shí)際渲染出任何html
- methods里邊聲明了validate方法,在測試代碼中會實(shí)際調(diào)用這里的聲明。注意stub的方法聲明無論如何都會返回true。這里依然還是按照上面提及的原則,單元測試僅測試與當(dāng)前函數(shù)相關(guān)的邏輯
此外。在shallowMount的參數(shù)中,使用stubs屬性內(nèi)聲明自定義的Form。
這樣,再次調(diào)用wrapper.vm.save 方法的時候,就不會報validate方法未定義了。
注意:業(yè)務(wù)碼中調(diào)用的 refs.xxxname,其中 xxxnames 為模板中組件元素的ref名。但stub的時候需要聲明的是組件的名稱。比如上面的例子,業(yè)務(wù)代碼中refname為xxxname,組件名為Form。這時候需要stub模擬的是Form,而不是xxxname。
Parent組件模擬
與上面的場景相反,這次我們討論如果業(yè)務(wù)代碼中有關(guān)于parent組件的調(diào)用應(yīng)該如何編寫測試代碼。
this.$parent.getList()
雖然我認(rèn)為直接調(diào)用parent的方法不是一種良好的實(shí)踐,但是誰讓vue提供了這樣的能力呢,你就不能保證沒有人會這么用。對于這種場景的測試代碼,原理跟上面一個場景是類似的,只是在shallowMount的調(diào)用中有一點(diǎn)不同,我們看具體代碼
const Parent = {
data: () => ({
val: true
}),
methods: {
getList: () => {} // 當(dāng)然如果業(yè)務(wù)代碼中需要依賴getList方法的返回,也可以在這里return result
},
template: '<div />'
}
// 接下來在shallowMount的時候聲明父組件即可
wrapper = shallowMount(indexTip, {
parentComponent: Parent, // 聲明父組件為上面邊定義的Parent
localVue,
store,
propsData: {}
})
mock異步請求
在測試中,經(jīng)常會碰到需要針對不同異步請求的結(jié)果編寫測試代碼的情況。
比如接口返回 code: 0 的時候做什么處理,code: 1的時候做什么處理,甚至是請求在網(wǎng)絡(luò)失敗的情況下做什么處理
function fn (xxx) {
return axios.get(url, {id: xxx})
.then(res => {
if (res.code === 10000) {
this.list = res.list
} else {
this.fail(res.errmsg)
}
})
.catch(err => {
this.toast(err.message)
})
}
如上代碼,我們要測試的函數(shù)叫做fn,其中調(diào)用了一個異步請求,那么我們在測試代碼中需要獲取到這個異步Promise對象才能對其內(nèi)部邏輯編寫測試代碼,因此fn函數(shù)需要返回這個Promise對象。
測試代碼如下
it('getMarkerList test case', async () => {
await wrapper.vm.fn(xxx)
expect(wrapper.vm.list).toBe([...somelist]) // 斷言此時vm.list 已被賦值成功
})
進(jìn)一步思考,既然是單元測試,是否不應(yīng)該依賴接口的返回,比如我們需要測試沒有網(wǎng)絡(luò)連接的情況下前端代碼的邏輯處理,總不能在持續(xù)集成過程中突然拔網(wǎng)線吧。
所以我們還需使用jest提供的mock promise能力
對Promise對象返回值的mock
上面介紹了mock Promise對象的方法,可以讓我們在沒有實(shí)際進(jìn)行網(wǎng)絡(luò)調(diào)用的情況下就觸發(fā)相應(yīng)的處理邏輯。
但大多數(shù)情況下,還要更加細(xì)分地對不同代碼分支做斷言。
比如
if (res.code === 10000)
...
else if (res.code === 10002) {
if (res.hasSelected === true)
...
else {
...
}
}
因此,jest提供了快捷的方法來進(jìn)行不同返回值的模擬
axios.get.mockResolvedValue({code: 10000, list: [1, 2, 3]}) // 模擬成功的狀態(tài)
await wrapper.vm.fn()
expect(wrapper.vm.list.length).toBe(3) // 斷言fn內(nèi)部promise調(diào)用成功時,list屬性應(yīng)該是一個三個元素的數(shù)組
axios.get.mockResolvedValue({code: 10001, msg: 'id not fount'}) // 模擬沒有找到該id的對應(yīng)記錄
await wrapper.vm.fn()
expect(wrapper.vm.fail).toBeCalledWith('id not found') // 斷言fn此場景下,fail方法被調(diào)用且參數(shù)為 ‘id not found’
axios.get.mockRejectedValue('networkError') // reject 的情況
await wrapper.vm.getLouDong()
expect(wrapper.vm.toast).toBeCalledWith('networkError')
模擬setTimeout
業(yè)務(wù)代碼中,可能會在方法內(nèi)部執(zhí)行setTimeout,過一定時間之后,再進(jìn)行某些操作,比如
submit () {
this.toast('清空中')
setTimeout(() => {
this.loaded = []
}, 300)
}
這個場景可能是出于用戶體驗(yàn)的考慮。
那么如果按照常規(guī)的方法,我們在執(zhí)行了getNewList方法之后就馬上去進(jìn)行斷言,則測試肯定是無法通過的。
針對這種情況,jest提供了一個有用的功能,useFakeTimers 。
在測試代碼中只需要聲明jest.useFakeTimers 然后再調(diào)用相應(yīng)方法,讓假的計時器向前或者向后若干毫秒即可,看代碼
it('submit test case', () => {
jest.useFakeTimers()
wrapper.vm.submit()
jest.advanceTimersByTime(350)
expect(wrapper.vm.loaded.length).toBe(0)
})
模擬window.location
在業(yè)務(wù)代碼中經(jīng)常會碰到需要使用當(dāng)前URL來進(jìn)行判斷的場景,但在jest中我們不能直接對window.location進(jìn)行賦值來模擬這個場景,如何做到?
比如我們需要根據(jù) location.search 中包含特定的參數(shù)值來執(zhí)行不同的代碼分支。
可以按照兩個步驟來進(jìn)行模擬:
- 自定義window對象,覆蓋jsdom中的window對象
- 通過defineProperty對window.location對象進(jìn)行賦值
global.window = Object.create(window);
const url = "http://dummy.com?foo=bar";
Object.defineProperty(window, 'location', {
value: {
href: url,
search: '?foo=bar'
}
});
expect(global.location.search.indexOf('foo') > -1).toBeTruthy()
注意:由于覆蓋了全局變量location, 因此要注意其只在某一個測試用例中起作用,否則會導(dǎo)致其他測試用例失敗。