本篇文章給大家分享的是有關(guān)如何在Jest中使用Vue-test-utils,小編覺得挺實(shí)用的,因此分享給大家學(xué)習(xí),希望大家閱讀完這篇文章后可以有所收獲,話不多說,跟著小編一起來看看吧。
專注于為中小企業(yè)提供成都網(wǎng)站制作、網(wǎng)站建設(shè)服務(wù),電腦端+手機(jī)端+微信端的三站合一,更高效的管理,為中小企業(yè)貢嘎免費(fèi)做網(wǎng)站提供優(yōu)質(zhì)的服務(wù)。我們立足成都,凝聚了一批互聯(lián)網(wǎng)行業(yè)人才,有力地推動(dòng)了超過千家企業(yè)的穩(wěn)健成長,幫助中小企業(yè)通過網(wǎng)站建設(shè)實(shí)現(xiàn)規(guī)模擴(kuò)充和轉(zhuǎn)變。
介紹
Vue-test-utils是Vue的官方的單元測(cè)試框架,它提供了一系列非常方便的工具,使我們更加輕松的為Vue構(gòu)建的應(yīng)用來編寫單元測(cè)試。主流的 JavaScript 測(cè)試運(yùn)行器有很多,但 Vue Test Utils 都能夠支持。它是測(cè)試運(yùn)行器無關(guān)的。
Jest,是由Facebook開發(fā)的單元測(cè)試框架,也是Vue推薦的測(cè)試運(yùn)行器之一。Vue對(duì)它的評(píng)價(jià)是:
Jest 是功能最全的測(cè)試運(yùn)行器。它所需的配置是最少的,默認(rèn)安裝了 JSDOM,內(nèi)置斷言且命令行的用戶體驗(yàn)非常好。不過你需要一個(gè)能夠?qū)挝募M件導(dǎo)入到測(cè)試中的預(yù)處理器。我們已經(jīng)創(chuàng)建了 vue-jest 預(yù)處理器來處理最常見的單文件組件特性,但仍不是 vue-loader 100% 的功能。
我認(rèn)為可以這樣理解,Vue-test-utils在Vue和Jest之前提供了一個(gè)橋梁,暴露出一些接口,讓我們更加方便的通過Jest為Vue應(yīng)用編寫單元測(cè)試。
安裝
通過Vue-cli創(chuàng)造模板腳手架時(shí),可以選擇是否啟用單元測(cè)試,并且選擇單元測(cè)試框架,這樣Vue就幫助我們自動(dòng)配置好了Jest。
如果是后期添加單元測(cè)試的話,首先要安裝Jest和Vue Test Utils:
npm install --save-dev jest @vue/test-utils
然后在package.json中定義一個(gè)單元測(cè)試的腳本。
// package.json { "scripts": { "test": "jest" } }
為了告訴Jest如何處理*.vue文件,需要安裝和配置vue-jest預(yù)處理器:
npm install --save-dev vue-jest
接下來在jest.conf.js配置文件中進(jìn)行配置:
module.exports = { moduleFileExtensions: ['js', 'json', 'vue'], moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1' }, transform: { '^.+\\.js$': '<rootDir>/node_modules/babel-jest', '.*\\.(vue)$': '<rootDir>/node_modules/vue-jest' }, }
其他的具體的配置可以參考官方文檔。
配置好了之后,就可以開始編寫單元測(cè)試了。
import { mount } from '@vue/test-utils' import Component from './component' describe('Component', () => { test('是一個(gè) Vue 實(shí)例', () => { const wrapper = mount(Component) expect(wrapper.isVueInstance()).toBeTruthy() }) })
上面的例子中,就是通過vue-test-utils提供的mount方法來掛載組件,創(chuàng)建包裹器和Vue實(shí)例
如果不使用vue-test-utils也是可以掛載組件的:
import Vue from 'vue'; import Test1 from '@/components/Test1'; const Constructor = Vue.extend(HelloWorld); const vm = new Constructor().$mount();
啟用單元測(cè)試的命令:
npm run unit
可以在后面加上-- --watch啟動(dòng)監(jiān)聽模式
別名配置
使用別名在Vue中很常見,可以讓我們避免使用復(fù)雜、易錯(cuò)的相對(duì)路徑:
import Page from '@/components/Test5/Test5'
上面的@就是別名,在使用Vue-cli搭建的項(xiàng)目中,默認(rèn)已經(jīng)在webpack.base.conf.js中對(duì)@進(jìn)行了配置:
module.exports = { ... resolve: { extensions: ['.js', '.vue', '.json'], alias: { 'vue$': 'vue/dist/vue.esm.js', '@': path.join(__dirname, '..', 'src') } }, }
同樣,使用Jest時(shí)也需要在Jest的配置文件jest.conf.js中進(jìn)行配置
"jest": { "moduleNameMapper": { '^@/(.*)$': "<rootDir>/src/$1", }, ...
Shallow Rendering
創(chuàng)建一個(gè)App.vue:
<template> <div id="app"> <Page :messages="messages"></Page> </div> </template> <script> import Page from '@/components/Test1' export default { name: 'App', data() { return { messages: ['Hello Jest', 'Hello Vue'] } }, components: { Page } } </script>
然后創(chuàng)建一個(gè)Test1組件
<template> <div> <p v-for="message in messages" :key="message">{{message}}</p> </div> </template> <script> export default { props: ['messages'], data() { return {} } } </script>
針對(duì)App.vue編寫單元測(cè)試文件App.spec.js
// 從測(cè)試實(shí)用工具集中導(dǎo)入 `mount()` 方法 import { mount } from 'vue-test-utils'; // 導(dǎo)入你要測(cè)試的組件 import App from '@/App'; describe('App.test.js', () => { let wrapper, vm; beforeEach(() => { wrapper = mount(App); vm = wrapper.vm; wrapper.setProps({ messages: ['Cat'] }) }); it('equals messages to ["Cat"]', () => { expect(vm.messages).toEqual(['Cat']) }); // 為App的單元測(cè)試增加快照(snapshot): it('has the expected html structure', () => { expect(vm.$el).toMatchSnapshot() }) });
執(zhí)行單元測(cè)試后,測(cè)試通過,然后Jest會(huì)在test/__snapshots__/文件夾下創(chuàng)建一個(gè)快照文件App.spec.js.snap
// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`App.test.js has the expected html structure 1`] = ` <div id="app" > <div> <p> Cat </p> </div> </div> `;
通過快照我們可以發(fā)現(xiàn),子組件Test1被渲染到App中了。
這里面有一個(gè)問題:單元測(cè)試應(yīng)該以獨(dú)立的單位進(jìn)行。也就是說,當(dāng)我們測(cè)試App時(shí),不需要也不應(yīng)該關(guān)注其子組件的情況。這樣才能保證單元測(cè)試的獨(dú)立性。比如,在created鉤子函數(shù)中進(jìn)行的操作就會(huì)給測(cè)試帶來不確定的問題。
為了解決這個(gè)問題,Vue-test-utils提供了shallow方法,它和mount一樣,創(chuàng)建一個(gè)包含被掛載和渲染的Vue組件的Wrapper,不同的創(chuàng)建的是被存根的子組件。
這個(gè)方法可以保證你關(guān)心的組件在渲染時(shí)沒有同時(shí)將其子組件渲染,避免了子組件可能帶來的副作用(比如Http請(qǐng)求等)
所以,將App.spec.js中的mount方法更改為shallow方法,再次查看快照
// Jest Snapshot v1, https://goo.gl/fbAQLP exports[`App.test.js has the expected html structure 1`] = ` <div id="app" > <!----> </div> `;
可以看出來,子組件沒有被渲染,這時(shí)候針對(duì)App.vue的單元測(cè)試就從組件樹中被完全隔離了。??
測(cè)試DOM結(jié)構(gòu)
通過mount、shallow、find、findAll方法都可以返回一個(gè)包裹器對(duì)象,包裹器會(huì)暴露很多封裝、遍歷和查詢其內(nèi)部的Vue組件實(shí)例的便捷的方法。
其中,find和findAll方法都可以都接受一個(gè)選擇器作為參數(shù),find方法返回匹配選擇器的DOM節(jié)點(diǎn)或Vue組件的Wrapper,findAll方法返回所有匹配選擇器的DOM節(jié)點(diǎn)或Vue組件的Wrappers的WrapperArray。
一個(gè)選擇器可以是一個(gè)CSS選擇器、一個(gè)Vue組件或是一個(gè)查找選項(xiàng)對(duì)象。
CSS選擇器:可以匹配任何有效的CSS選擇器
標(biāo)簽選擇器 (div、foo、bar)
類選擇器 (.foo、.bar)
特性選擇器 ([foo]、[foo="bar"])
id 選擇器 (#foo、#bar)
偽選擇器 (div:first-of-type)
符合選擇器(div > #bar > .foo、div + .foo)
Vue組件:Vue 組件也是有效的選擇器。
查找選項(xiàng)對(duì)象:
Name:可以根據(jù)一個(gè)組件的name選擇元素。wrapper.find({ name: 'my-button' })
Ref:可以根據(jù)$ref選擇元素。wrapper.find({ ref: 'myButton' })
這樣我們就可以對(duì)DOM的結(jié)構(gòu)進(jìn)行驗(yàn)證:
describe('Test for Test1 Component', () => { let wrapper, vm; beforeEach(() => { // wrapper = mount(App); wrapper = shallow(Test1, { propsData: { messages: ['bye'] } }); }); it('is a Test1 component', () => { // 使用Vue組件選擇器 expect(wrapper.is(Test1)).toBe(true); // 使用CSS選擇器 expect(wrapper.is('.outer')).toBe(true); // 使用CSS選擇器 expect(wrapper.contains('p')).toBe(true) }); });
還可以進(jìn)行一步對(duì)DOM結(jié)構(gòu)進(jìn)行更細(xì)致的驗(yàn)證:
// exists():斷言 Wrapper 或 WrapperArray 是否存在。 it('不存在img', () = > { expect(wrapper.findAll('img').exists()).toBeFalsy() }); // isEmpty():斷言 Wrapper 并不包含子節(jié)點(diǎn)。 it('MyButton組件不為空', () = > { expect(wrapper.find(MyButton).isEmpty()).toBeFalsy() }); // attributes():返回 Wrapper DOM 節(jié)點(diǎn)的特性對(duì)象 // classes():返回 Wrapper DOM 節(jié)點(diǎn)的 class 組成的數(shù)組 it('MyButton組件有my-class類', () = > { expect(wrapper.find(MyButton).attributes().class).toContain('my-button'); expect(wrapper.find(MyButton).classes()).toContain('my-button'); })
測(cè)試樣式
UI的樣式測(cè)試為了測(cè)試我們的樣式是否復(fù)合設(shè)計(jì)稿預(yù)期。同時(shí)通過樣式測(cè)試我們可以感受當(dāng)我們code變化帶來的UI變化,以及是否符合預(yù)期。
inline style :如果樣式是inline style,可以使用hasStyle來驗(yàn)證,也可以使用Jest的Snapshot Testing最方便。
// hasStyle:判斷是否有對(duì)應(yīng)的內(nèi)聯(lián)樣式 it('MyButton組件有my-class類', () = > { expect(wrapper.find(MyButton).hasStyle('padding-top', '10')).toBeTruthy() })
CSS:屬于E2E測(cè)試,把整個(gè)系統(tǒng)當(dāng)作一個(gè)黑盒,只有UI會(huì)暴露給用戶用來測(cè)試一個(gè)應(yīng)用從頭到尾的流程是否和設(shè)計(jì)時(shí)候所想的一樣 。有專門的E2E測(cè)試框架。比較流行的E2E測(cè)試框架有nightwatch等,關(guān)于E2E測(cè)試框架的介紹可以參考這篇文章。
測(cè)試Props
父組件向子組件傳遞數(shù)據(jù)使用Props,而子組件向父組件傳遞數(shù)據(jù)則需要在子組件出發(fā)父組件的自定義事件
當(dāng)測(cè)試對(duì)父組件向子組件傳遞數(shù)據(jù)這一行為時(shí),我們想要測(cè)試的當(dāng)我們傳遞給子組件一個(gè)特定的參數(shù),子組件是否會(huì)按照我們所斷言的那樣變現(xiàn)。
在初始化時(shí)向子組件傳值,使用的方法是propsData。
const wrapper = mount(Foo, { propsData: { foo: 'bar' } })
也可以使用setProps方法:
const wrapper = mount(Foo) wrapper.setProps({ foo: 'bar' })
我們傳遞給Test1組件的messages一個(gè)['bye']數(shù)組,來驗(yàn)證是否存在:
beforeEach(() = > { wrapper = mount(Test1, { propsData: { messages: ['bye'] } }); }); // props:返回 Wrapper vm 的 props 對(duì)象。 it('接收到了bye作為Props', () = > { expect(wrapper.props().messages).toContain('bye') });
有時(shí)候會(huì)對(duì)Props的Type、默認(rèn)值或者通過validator對(duì)Prop進(jìn)行自定義的驗(yàn)證
props: { messages: { type: Array, required: true, validator: (messages) = > messages.length > 1, default () { return [0, 2] } } },
通過Vue實(shí)例的$options獲取包括Props在內(nèi)的初始化選項(xiàng):
// vm.$options返回Vue實(shí)例的初始化選項(xiàng) describe('驗(yàn)證Props的各個(gè)屬性', () = > { wrapper = mount(Test1, { propsData: { messages: ['bye', 'bye', 'bye'] } }); const messages = wrapper.vm.$options.props.messages; it('messages is of type array', () = > { expect(messages.type).toBe(Array) }); it('messages is required', () = > { expect(messages.required).toBeTruthy() }); it('messages has at least length 2', () = > { expect(messages.validator && messages.validator(['a'])).toBeFalsy(); expect(messages.validator && messages.validator(['a', 'a'])).toBeTruthy(); }); wrapper.destroy() });
測(cè)試自定義事件
自定義事件要測(cè)試點(diǎn)至少有以下兩個(gè):
測(cè)試事件會(huì)被正常觸發(fā)
測(cè)試事件被觸發(fā)后的后續(xù)行為符合預(yù)期
具體到Test1組件和MyButton組件來看:
TEST1組件:
// TEST1 <MyButton class="my-button" buttonValue="Me" @add="addCounter"></MyButton> // 省略一些代碼 methods: { addCounter(value) { this.count = value } },
MyButton組件:
<button @click="increment">Click {{buttonValue}} {{innerCount}}</button>、 // 省略一些代碼 data() { return { innerCount: 0 } }, computed: {}, methods: { increment() { this.innerCount += 1; this.$emit('add', this.innerCount) } },
要測(cè)試的目的是:
1. 當(dāng)MyButton組件的按鈕被點(diǎn)擊后會(huì)觸發(fā)increment事件
2. 點(diǎn)擊事件發(fā)生后,Test1組件的addCounter函數(shù)會(huì)被觸發(fā)并且結(jié)果符合預(yù)期(及數(shù)字遞增)
首先為MyButton編寫單元測(cè)試文件:
describe('Test for MyButton Component', () => { const wrapper = mount(MyButton); it('calls increment when click on button', () => { // 創(chuàng)建mock函數(shù) const mockFn = jest.fn(); // 設(shè)置 Wrapper vm 的方法并強(qiáng)制更新。 wrapper.setMethods({ increment: mockFn }); // 觸發(fā)按鈕的點(diǎn)擊事件 wrapper.find('button').trigger('click'); expect(mockFn).toBeCalled(); expect(mockFn).toHaveBeenCalledTimes(1) }) });
通過setMethods方法用mock函數(shù)代替真實(shí)的方法,然后就可以斷言點(diǎn)擊按鈕后對(duì)應(yīng)的方法有沒有被觸發(fā)、觸發(fā)幾次、傳入的參數(shù)等等。
現(xiàn)在我們測(cè)試了點(diǎn)擊事件后能觸發(fā)對(duì)應(yīng)的方法,下面要測(cè)試的就是increment方法將觸發(fā)Test1組件中自定義的add方法
// increment方法會(huì)觸發(fā)add方法 it('triggers a addCounter event when a handleClick method is called', () = > { const wrapper = mount(MyButton); // mock自定義事件 const mockFn1 = jest.fn(); wrapper.vm.$on('add', mockFn1); // 觸發(fā)按鈕的點(diǎn)擊事件 wrapper.find('button').trigger('click'); expect(mockFn1).toBeCalled(); expect(mockFn1).toHaveBeenCalledWith(1); // 再次觸發(fā)按鈕的點(diǎn)擊事件 wrapper.find('button').trigger('click'); expect(mockFn1).toHaveBeenCalledTimes(2); expect(mockFn1).toHaveBeenCalledWith(2); })
這里使用了$on方法,將Test1自定義的add事件替換為Mock函數(shù)
對(duì)于自定義事件,不能使用trigger方法觸發(fā),因?yàn)閠rigger只是用DOM事件。自定義事件使用$emit觸發(fā),前提是通過find找到MyButton組件
// $emit 觸發(fā)自定義事件 describe('驗(yàn)證addCounter是否被觸發(fā)', () = > { wrapper = mount(Test1); it('addCounter Fn should be called', () = > { const mockFn = jest.fn(); wrapper.setMethods({ 'addCounter': mockFn }); wrapper.find(MyButton).vm.$emit('add', 100); expect(mockFn).toHaveBeenCalledTimes(1); }); wrapper.destroy() });
測(cè)試計(jì)算屬性
創(chuàng)建Test2組件,實(shí)現(xiàn)功能是使用計(jì)算屬性將輸入框輸入的字符翻轉(zhuǎn):
<template> <div class="wrapper"> <label for="input">輸入:</label> <input id="input" type="text" v-model="inputValue"> <p>輸出:{{outputValue}}</p> </div> </template> <script> export default { name: 'Test2', props: { needReverse: { type: Boolean, default: false } }, data() { return { inputValue: '' } }, computed: { outputValue () { return this.needReverse ? ([...this.inputValue]).reverse().join('') : this.inputValue } }, methods: {}, components: {} } </script> <style scoped> .wrapper { width: 300px; margin: 0 auto; text-align: left; } </style>
在Test2.spec.js中,可以通過wrapper.vm屬性訪問一個(gè)實(shí)例所有的方法和屬性。這只存在于 Vue 組件包裹器中。
describe('Test for Test2 Component', () => { let wrapper; beforeEach(() => { wrapper = shallow(Test2); }); afterEach(() => { wrapper.destroy() }); it('returns the string in normal order if reversed property is not true', () => { wrapper.setProps({needReverse: false}); wrapper.vm.inputValue = 'ok'; expect(wrapper.vm.outputValue).toBe('ok') }); it('returns the string in normal order if reversed property is not provided', () => { wrapper.vm.inputValue = 'ok'; expect(wrapper.vm.outputValue).toBe('ok') }); it('returns the string in reversed order if reversed property is true', () => { wrapper.setProps({needReverse: true}); wrapper.vm.inputValue = 'ok'; expect(wrapper.vm.outputValue).toBe('ko') }) });
測(cè)試監(jiān)聽器
Vue提供的watch選項(xiàng)提供了一個(gè)更通用的方法,來響應(yīng)數(shù)據(jù)的變化。
為Test添加偵聽器:
watch: { inputValue: function(newValue, oldValue) { if (newValue.trim().length > 0 && newValue !== oldValue) { this.printNewValue(newValue) } } }, methods: { printNewValue(value) { console.log(value) } },
為了測(cè)試,首先開始測(cè)試前將console的log方法用jest的spyOn方法mock掉,最好在測(cè)試結(jié)束后通過mockClear方法將其重置,避免無關(guān)狀態(tài)的引入。
describe('Test watch', () = > { let spy; beforeEach(() = > { wrapper = shallow(Test2); spy = jest.spyOn(console, 'log') }); afterEach(() = > { wrapper.destroy(); spy.mockClear() }); }
然后執(zhí)行給inputValue賦值,按照預(yù)期,spy方法會(huì)被調(diào)用
it('is called with the new value in other cases', () = > { wrapper.vm.inputValue = 'ok'; expect(spy).toBeCalled() });
但是在執(zhí)行之后我們發(fā)現(xiàn)并非如此,spy并未被調(diào)用,原因是:
watch中的方法被Vue**推遲**到了更新的下一個(gè)循環(huán)隊(duì)列中去異步執(zhí)行,如果這個(gè)watch被觸發(fā)多次,只會(huì)被推送到隊(duì)列一次。這種緩沖行為可以有效的去掉重復(fù)數(shù)據(jù)造成的不必要的性能開銷。
所以當(dāng)我們?cè)O(shè)置了inputValue為'ok'之后,watch中的方法并沒有立刻執(zhí)行,但是expect卻執(zhí)行了,所以斷言失敗了。
解決方法就是將斷言放到$nextTick中,在下一個(gè)循環(huán)隊(duì)列中執(zhí)行,同時(shí)在expect后面執(zhí)行Jest提供的done()方法,Jest會(huì)等到done()方法被執(zhí)行才會(huì)結(jié)束測(cè)試。
it('is called with the new value in other cases', (done) = > { wrapper.vm.inputValue = 'ok'; wrapper.vm.$nextTick(() = > { expect(spy).toBeCalled(); done() }) });
在測(cè)試第二個(gè)情況時(shí),由于對(duì)inputValue賦值時(shí)spy會(huì)被執(zhí)行一次,所以需要清除spy的狀態(tài),這樣才能得出正確的預(yù)期:
it('is not called with same value', (done) = > { wrapper.vm.inputValue = 'ok'; wrapper.vm.$nextTick(() = > { // 清除已發(fā)生的狀態(tài) spy.mockClear(); wrapper.vm.inputValue = 'ok'; wrapper.vm.$nextTick(() = > { expect(spy).not.toBeCalled(); done() }) }) });
測(cè)試方法
單元測(cè)試的核心之一就是測(cè)試方法的行為是否符合預(yù)期,在測(cè)試時(shí)要避免一切的依賴,將所有的依賴都mock掉。
創(chuàng)建Test3組件,輸入問題后,點(diǎn)擊按鈕后,使用axios發(fā)送HTTP請(qǐng)求,獲取答案
<template> <div class="wrapper"> <label for="input">問題:</label> <input id="input" type="text" v-model="inputValue"> <button @click="getAnswer">click</button> <p>答案:{{answer}}</p> <img :src="src"> </div> </template> <script> import axios from 'axios'; export default { name: 'Test3', data() { return { inputValue: 'ok?', answer: '', src: '' } }, methods: { getAnswer() { const URL = 'https://yesno.wtf/api'; return axios.get(URL).then(result => { if (result && result.data) { this.answer = result.data.answer; this.src = result.data.image; return result } }).catch(e => {}) } } } </script> <style scoped> .wrapper { width: 500px; margin: 0 auto; text-align: left; } </style>
這個(gè)例子里面,我們僅僅關(guān)注測(cè)試getAnswer方法,其他的忽略掉。為了測(cè)試這個(gè)方法,我們需要做的有:
我們不需要實(shí)際調(diào)用axios.get方法,需要將它mock掉
我們需要測(cè)試是否調(diào)用了axios方法(但是并不實(shí)際觸發(fā))并且返回了一個(gè)Promise對(duì)象
返回的Promise對(duì)象執(zhí)行了回調(diào)函數(shù),設(shè)置用戶名和頭像
我們現(xiàn)在要做的就是mock掉外部依賴。Jest提供了一個(gè)很好的mock系統(tǒng),讓我們能夠很輕易的mock所有依賴,前面我們用過jest.spyOn方法和jest.fn方法,但對(duì)于上面的例子來說,僅使用這兩個(gè)方法是不夠的。
我們現(xiàn)在要mock掉整個(gè)axios模塊,使用的方法是jest.mock,就可以mock掉依賴的模塊。
jest.mock('dependency-path', implementationFunction)
在Test3.spec.js中,首先將axios中的get方法替換為我們的mock函數(shù),然后引入相應(yīng)的模塊
jest.mock('axios', () => ({ get: jest.fn() })); import { shallow } from 'vue-test-utils'; import Test3 from '@/components/Test3'; import axios from 'axios';
然后測(cè)試點(diǎn)擊按鈕后,axios的get方法是否被調(diào)用:
describe('Test for Test3 Component', () => { let wrapper; beforeEach(() => { axios.get.mockClear(); wrapper = shallow(Test3); }); afterEach(() = > { wrapper.destroy() }); // 點(diǎn)擊按鈕后調(diào)用了 getAnswer 方法 it('getAnswer Fn should be called', () => { const mockFn = jest.fn(); wrapper.setMethods({getAnswer: mockFn}); wrapper.find('button').trigger('click'); expect(mockFn).toBeCalled(); }); // 點(diǎn)擊按鈕后調(diào)用了axios.get方法 it('axios.get Fn should be called', () => { const URL = 'https://yesno.wtf/api'; wrapper.find('button').trigger('click'); expect(axios.get).toBeCalledWith(URL) }); });
測(cè)試結(jié)果發(fā)現(xiàn),雖然我們的mock函數(shù)被調(diào)用了,但是控制臺(tái)還是報(bào)錯(cuò)了,原因是我們mock的axios.get方法雖然被調(diào)用了,但是并沒有返回任何值,所以報(bào)錯(cuò)了,所以下一步我們要給get方法返回一個(gè)Promise,查看方法能否正確處理我們返回的數(shù)據(jù)
jest.fn()接受一個(gè)工廠函數(shù)作為參數(shù),這樣就可以定義其返回值
const mockData = { data: { answer: 'mock_yes', image: 'mock.png' } }; jest.mock('axios', () => ({ get: jest.fn(() => Promise.resolve(mockData)) }));
getAnswer是一個(gè)異步請(qǐng)求,Jest提供的解決異步代碼測(cè)試的方法有以下三種:
回調(diào)函數(shù)中使用done()參數(shù)
Pomise
Aysnc/Await
第一種是使用在異步請(qǐng)求的回調(diào)函數(shù)中使用Jest提供的叫做done的單參數(shù),Jest會(huì)等到done()執(zhí)行結(jié)束后才會(huì)結(jié)束測(cè)試。
我們使用第二種和第三種方法來測(cè)試getAnswer方法的返回值,前提就是在方法中返回一個(gè)Promise。(一般來說,在被測(cè)試的方法中給出一個(gè)返回值會(huì)讓測(cè)試更加容易)。 Jest會(huì)等待Promise解析完成。 如果承諾被拒絕,則測(cè)試將自動(dòng)失敗。
// axios.get方法返回值(Promise) it('Calls get promise result', () = > { return expect(wrapper.vm.getAnswer()).resolves.toEqual(mockData); });
或者可以使用第三種方法,也就是使用async和await來測(cè)試異步代碼:
// 可以用 Async/Await 測(cè)試 axios.get 方法返回值 it('Calls get promise result 3', async() = > { const result = await wrapper.vm.getAnswer(); expect(result).toEqual(mockData) });
Jest都提供了resolves和rejects方法作為then和catch的語法糖:
it('Calls get promise result 2', () = > { return wrapper.vm.getAnswer().then(result = > { expect(result).toEqual(mockData); }) }); it('Calls get promise result 4', async() = > { await expect(wrapper.vm.getAnswer()).resolves.toEqual(mockData) });
mock依賴
我們可以創(chuàng)建一個(gè)__mocks__文件夾,將mock文件放入其中,這樣就不必在每個(gè)測(cè)試文件中去單獨(dú)的手動(dòng)mock模塊的依賴
在__mocks__文件夾下創(chuàng)建axios.js文件:
// test/__mocks__/axios.js const mock = { get: jest.fn(() => Promise.resolve({ data: { answer: 'mock_yes', image: 'mock.png' } })) }; export default mock
這樣就可以將Test3.spec.js中的jest.mock部分代碼移除了。Jest會(huì)自動(dòng)在__mocks__文件夾下尋找mock的模塊,但是有一點(diǎn)要注意,模塊的注冊(cè)和狀態(tài)會(huì)一直被保存,所有如果我們?cè)赥est3.spec.js最后增加一條斷言:
// 如果不清除模塊狀態(tài)此條斷言會(huì)失敗 it('Axios should not be called here', () = > { expect(axios.get).not.toBeCalled() });
因?yàn)槲覀冊(cè)赽eforeEach中添加了axios.get的狀態(tài)清除的語句 axios.get.mockClear(),所以上面的斷言會(huì)通過,否則會(huì)失敗。
也可以用另外resetModules和clearAllMocks來確保每次開始前都重置模塊和mock依賴的狀態(tài)。
beforeEach(() = > { wrapper = shallow(Test3); jest.resetModules(); jest.clearAllMocks(); });
我們?cè)陧?xiàng)目中有時(shí)候會(huì)根據(jù)需要對(duì)不同的Http請(qǐng)求的數(shù)據(jù)進(jìn)行Mock,以MockJS為例,一般每個(gè)組件(模塊)都有對(duì)應(yīng)的mock文件,然后通過index.js導(dǎo)入到系統(tǒng)。Jest也可以直接將MockJS的數(shù)據(jù)導(dǎo)入,只需要在setup.js中導(dǎo)入MockJS的index.js文件即可
測(cè)試插槽
插槽(slots)用來在組件中插入、分發(fā)內(nèi)容。創(chuàng)建一個(gè)使用slots的組件Test4
// TEST4 <MessageList> <Message v-for="message in messages" :key="message" :message="message"></Message> </MessageList> // MessageList <ul class="list-messages"> <slot></slot> </ul> // Message <li>{{message}}</li>
在測(cè)試slots時(shí),我們的關(guān)注點(diǎn)是slots中的內(nèi)容是否在組件中出現(xiàn)在該出現(xiàn)的位置,測(cè)試方法和前面介紹的測(cè)試DOM結(jié)構(gòu)的方法相同。
具體到例子中來看,我們要測(cè)試的是:Message組件是否出現(xiàn)在具有l(wèi)ist-messages的類的ul中。在測(cè)試時(shí),為了將slots傳遞給MessageList組件,我們?cè)贛essageList.spec.js中的mount或者shallow方法中使用slots屬性
import { mount } from 'vue-test-utils'; import MessageList from '@/components/Test4/MessageList'; describe('Test for MessageList of Test4 Component', () => { let wrapper; beforeEach(() => { wrapper = mount(MessageList, { slots: { default: '<div class="fake-msg"></div>' } }); }); afterEach(() => { wrapper.destroy() }); // 組件中應(yīng)該通過slots插入了div.fake-msg it('Messages are inserted in a ul.list-messages element', () => { const list = wrapper.find('ul.list-messages'); expect(list.contains('div.fake-msg')).toBeTruthy() }) });
為了測(cè)試內(nèi)容是否通過插槽插入了組件,所以我們偽造了一個(gè)div.fake-msg通過slots選項(xiàng)傳入MessageList組件,斷言組件中應(yīng)該存在這個(gè)div
不僅如此,slots選項(xiàng)還可以傳入組件或者數(shù)組:
import AnyComponent from 'anycomponent' mount(MessageList, { slots: { default: AnyComponent // or [AnyComponent, AnyComponent] } })
這里面有一個(gè)問題,例如我們想測(cè)試Message組件是否通過插槽插入了MessageList組件中,我們可以將slots選項(xiàng)中傳入Message組件,但是由于Message組件需要傳入message作為Props,所以按照上面的說明,我們應(yīng)該這樣做:
beforeEach(() = > { const fakeMessage = mount(Message, { propsData: { message: 'test' } }); wrapper = mount(MessageList, { slots: { default: fakeMessage } }) });
對(duì)應(yīng)的斷言是:
// 組件中應(yīng)該通過slots插入了Message,并且傳入的文本是test it('Messages are inserted in a ul.list-messages element', () = > { const list = wrapper.find('ul.list-messages'); expect(list.contains('li')).toBeTruthy(); expect(list.find('li').text()).toBe('test') })
但是這會(huì)失敗,查了資料,貌似不能通過這種方式mounted的組件傳入slots中。
雖然如此,我們可以而通過渲染函數(shù)(render function)來作為一種非正式的解決方法:
const fakeMessage = { render(h) { return h(Message, { props: { message: 'test' } }) } }; wrapper = mount(MessageList, { slots: { default: fakeMessage } })
測(cè)試命名插槽(Named Slots)
測(cè)試命名插槽和默認(rèn)插槽原理相同,創(chuàng)建Test5組件,里面應(yīng)用新的MessageList組件,組件中增加一個(gè)給定名字為header的插槽,并設(shè)定默認(rèn)內(nèi)容:
<div> <header class="list-header"> <slot name="header">This is a default header</slot> </header> <ul class="list-messages"> <slot></slot> </ul> </div>
在Test5中就可以使用這個(gè)命名插槽:
<MessageList> <header slot="header">Awesome header</header> <Message v-for="message in messages" :key="message" :message="message"></Message> </MessageList>
對(duì)MessageList組件進(jìn)行測(cè)試時(shí),首先測(cè)試組件中是否渲染了命名插槽的默認(rèn)內(nèi)容:
// 渲染命名插槽的默認(rèn)內(nèi)容 it('Header slot renders a default header text', () = > { const header = wrapper.find('.list-header'); expect(header.text()).toBe('This is a default header') });
然后測(cè)試插槽是否能插入我們給定的內(nèi)容,只需要將mount方法中的slots選項(xiàng)的鍵值default改為被測(cè)試的插槽的name即可:
// 向header插槽中插入內(nèi)容 it('Header slot is rendered withing .list-header', () = > { wrapper = mount(MessageList, { slots: { header: '<header>What an awesome header</header>' } }); const header = wrapper.find('.list-header'); expect(header.text()).toBe('What an awesome header') })
測(cè)試debounce
我們經(jīng)常使用lodash的debounce方法,來避免一些高頻操作導(dǎo)致的函數(shù)在短時(shí)間內(nèi)被反復(fù)執(zhí)行,比如在Test6組件中,對(duì)button的點(diǎn)擊事件進(jìn)行了debounce,頻率為500ms,這就意味著如果在500ms內(nèi)如果用戶再次點(diǎn)擊按鈕,handler方法會(huì)被推遲執(zhí)行:
<template> <div class="outer"> <p>This button has been clicked {{count}}</p> <button @click="addCounter">click</button> </div> </template> <script> import _ from 'lodash'; export default { data() { return { count: 0 } }, methods: { addCounter: _.debounce(function () { this.handler() }, 500), handler() { this.count += 1; } } } </script>
在編寫Test6的單元測(cè)試時(shí),我們有一個(gè)這樣的預(yù)期:當(dāng)addCounter方法被觸發(fā)時(shí),500ms內(nèi)沒有任何后續(xù)操作,handler方法會(huì)被觸發(fā)
如果沒有進(jìn)行特殊的處理,單元測(cè)試文件應(yīng)該是這樣的:
import { shallow } from 'vue-test-utils'; import Test6 from '@/components/Test6'; describe('Test for Test6 Component', () => { let wrapper; beforeEach(() => { wrapper = shallow(Test6); }); afterEach(() => { wrapper.destroy() }); it('test for lodash', () => { const mockFn2 = jest.fn(); wrapper.setMethods({ handler: mockFn2 }); wrapper.vm.addCounter(); expect(mockFn2).toHaveBeenCalledTimes(1); }) });
測(cè)試結(jié)果發(fā)現(xiàn),addCounter被觸發(fā)時(shí)handler方法并沒有執(zhí)行
因?yàn)閘odash中debounce方法涉及到了setTimeout,`hanlder方法應(yīng)該是在500ms后執(zhí)行,所以在此時(shí)執(zhí)行時(shí)方法沒有執(zhí)行。
所以我們需要在Jest中對(duì)setTimeout進(jìn)行特殊的處理:Jest提供了相關(guān)的方法,我們需要使用的是jest.useFakeTimers()和jest.runAllTimers()
前者是用來讓Jest模擬我們用到的諸如setTimeout、setInterval等計(jì)時(shí)器,而后者是執(zhí)行setTimeout、setInterval等異步任務(wù)中的宏任務(wù)(macro-task)并且將需要的新的macro-task放入隊(duì)列中并執(zhí)行,更多信息的可以參考官網(wǎng)的timer-mocks。
所以對(duì)test6.spec.js進(jìn)行修改,在代碼開始增加jest.useFakeTimers(),在觸發(fā)addCounter方法后通過jest.runAllTimers()觸發(fā)macor-task任務(wù)
jest.useFakeTimers(); import { shallow } from 'vue-test-utils'; import Test6 from '@/components/Test6'; import _ from 'lodash'; describe('Test for Test6 Component', () => { let wrapper; beforeEach(() => { wrapper = shallow(Test6); }); afterEach(() => { wrapper.destroy() }); it('test for lodash', () => { const mockFn2 = jest.fn(); wrapper.setMethods({ handler: mockFn2 }); wrapper.vm.addCounter(); jest.runAllTimers(); expect(mockFn2).toHaveBeenCalledTimes(1); }) });
結(jié)果還是失敗,報(bào)錯(cuò)原因是:
Ran 100000 timers, and there are still more! Assuming we've hit an infinite recursion and bailing out…
程序陷入了死循環(huán),換用Jest提供額另外一個(gè)API:jest.runOnlyPendingTimers(),這個(gè)方法只會(huì)執(zhí)行當(dāng)前隊(duì)列中的macro-task,遇到的新的macro-task則不會(huì)被執(zhí)行
將jest.runAllTimers()替換為jest.runOnlyPendingTimers()后,上面的錯(cuò)誤消失了,但是handler仍然沒有被執(zhí)行
在查了許多資料后,這可能是lodash的debounce機(jī)制與jest的timer-mocks 無法兼容,如果有人能夠解決這個(gè)問題希望能夠指教。
這樣的情況下,我們退而求其次,我們不去驗(yàn)證addCounter是否會(huì)被debounce,因?yàn)閐ebounce是第三方模塊的方法,我們默認(rèn)認(rèn)為是正確的,我們要驗(yàn)證的是addCounter能夠正確觸發(fā)handler方法即可。
所以我們可以另辟蹊徑,通過mock將lodash的debounce修改為立即執(zhí)行的函數(shù),我們要做的是為lodash的debounce替換為jest.fn(),并且提供一個(gè)工廠函數(shù),返回值就是傳入的函數(shù)
import _ from 'lodash'; jest.mock('lodash', () => ({ debounce: jest.fn((fn => fn)) }));
在如此修改后,測(cè)試通過,handler方法正確執(zhí)行
同一個(gè)方法的多次mock
在一個(gè)組件中,我們可能會(huì)多次用到同一個(gè)外部的方法,但是每次返回值是不同的,我們可能要對(duì)它進(jìn)行多次不同的mock
舉個(gè)例子,在組件Test7中,mounted的時(shí)候forData返回一個(gè)數(shù)組,經(jīng)過map處理后賦給text,點(diǎn)擊getResult按鈕,返回一個(gè)0或1的數(shù)字,根據(jù)返回值為result賦值
<template> <div class="outer"> <p>{{text}}</p> <p>Result is {{result}}</p> <button @click="getResult">getResult</button> </div> </template> <script> import { forData } from '@/helper'; import axios from 'axios' export default { data() { return { text: '', result: '' } }, async mounted() { const ret = await forData(axios.get('text.do')); this.text = ret.map(val => val.name) }, methods: { async getResult() { const res = await forData(axios.get('result.do')); switch (res) { case 0 : { this.result = '000'; break } case 1 : { this.result = '111'; break } } }, } } </script>
針對(duì)getResult方法編寫單元測(cè)試,針對(duì)兩種返回值編寫了兩個(gè)用例,在用例中將forData方法mock掉,返回值是一個(gè)Promise值,再根據(jù)給定的返回值,判斷結(jié)果是否符合預(yù)期:
describe('Test for Test7 Component', () => { let wrapper; beforeEach(() => { wrapper = shallow(Test7); }); afterEach(() => { wrapper.destroy() }); it('test for getResult', async () => { // 設(shè)定forData返回值 const mockResult = 0; const mockFn = jest.fn(() => (Promise.resolve(mockResult))); helper.forData = mockFn; // 執(zhí)行 await wrapper.vm.getResult(); // 斷言 expect(mockFn).toHaveBeenCalledTimes(1); expect(wrapper.vm.result).toBe('000') }); it('test for getResult', async () => { // 設(shè)定forData返回值 const mockResult = 1; const mockFn = jest.fn(() => (Promise.resolve(mockResult))); helper.forData = mockFn; // 執(zhí)行 await wrapper.vm.getResult(); // 斷言 expect(mockFn).toHaveBeenCalledTimes(1); expect(wrapper.vm.result).toBe('111') }) });
運(yùn)行測(cè)試用例,雖然測(cè)試用例全部通過,但是控制臺(tái)仍然報(bào)錯(cuò)了:
(node:17068) UnhandledPromiseRejectionWarning: TypeError: ret.map is
not a function
為什么呢?
原因就是在于,在第一個(gè)用例運(yùn)行之后,代碼中的forData方法被我們mock掉了,所以在運(yùn)行第二個(gè)用例的時(shí)候,執(zhí)行mounted的鉤子函數(shù)時(shí),forData返回值就是我們?cè)谏蟼€(gè)用例中給定的1,所以使用map方法會(huì)報(bào)錯(cuò)
為了解決這個(gè)問題,我們需要在beforeEach(或afterEach)中,重置forData的狀態(tài),如果在代碼中使用了MockJS的情況下,我們只需要讓默認(rèn)的forData獲取的數(shù)據(jù)走原來的路徑,由MockJS提供假數(shù)據(jù)即可,這樣我們只需要在一代碼的最開始將forData保存,在beforeEach使用restoreAllMocks方法重置狀態(tài),然后在恢復(fù)forData狀態(tài),然后每個(gè)用例中針對(duì)forData進(jìn)行單獨(dú)的mock即可
const test = helper.forData; describe('Test for Test7 Component', () => { let wrapper; beforeEach(() => { jest.restoreAllMocks(); helper.forData = test; wrapper = shallow(Test7); }); afterEach(() => { wrapper.destroy() }); // 用例不變
如果沒有使用MockJS,那么都需要我們提供數(shù)據(jù),就需要在afterEach中提供mounted時(shí)需要的數(shù)據(jù):
beforeEach(() = > { jest.restoreAllMocks(); const mockResult = [{ name: 1}, {name: 2}]; helper.forData = jest.fn(() = > (Promise.resolve(mockResult))); wrapper = shallow(Test7); });
這樣處理過后,運(yùn)行用例通過,并且控制臺(tái)也不會(huì)報(bào)錯(cuò)了。
如果是在同一個(gè)方法中遇到了需要不同返回結(jié)果的forData,比如下面的getQuestion方法:
async getQuestion() { const r1 = await forData(axios.get('result1.do')); const r2 = await forData(axios.get('result2.do')); const res = r1 + r2; switch (res) { case 2: { this.result = '222'; break } case 3: { this.result = '333'; break } } },
通過forData發(fā)出了兩個(gè)不同的HTTP請(qǐng)求,返回結(jié)果不同,這時(shí)我們?cè)跍y(cè)試時(shí)就需要使用mockImplementationOnce方法,這個(gè)方法mock的函數(shù)只被調(diào)用一次,多次調(diào)用時(shí)就會(huì)根據(jù)定義時(shí)的順序依次調(diào)用mock函數(shù),所以測(cè)試用例如下:
it('test for getQuestion', async() = > { // 設(shè)定forData返回值 const mockFn = jest.fn() .mockImplementationOnce(() = > (Promise.resolve(1))) .mockImplementationOnce(() = > (Promise.resolve(2))); helper.forData = mockFn; // 執(zhí)行 await wrapper.vm.getQuestion(); // 斷言 expect(mockFn).toHaveBeenCalledTimes(2); expect(wrapper.vm.result).toBe('333') });
以上就是如何在Jest中使用Vue-test-utils,小編相信有部分知識(shí)點(diǎn)可能是我們?nèi)粘9ぷ鲿?huì)見到或用到的。希望你能通過這篇文章學(xué)到更多知識(shí)。更多詳情敬請(qǐng)關(guān)注創(chuàng)新互聯(lián)行業(yè)資訊頻道。
分享標(biāo)題:如何在Jest中使用Vue-test-utils
鏈接分享:http://www.muchs.cn/article36/ghposg.html
成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供網(wǎng)站營銷、面包屑導(dǎo)航、虛擬主機(jī)、電子商務(wù)、外貿(mào)建站、品牌網(wǎng)站制作
聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)