Test Methods and Mock Dependencies in Vue.js with Jest
用Jest測(cè)試Vue中的Methods中的方法和Mock依賴
Learn how to test methods and cope with mocking module dependencies.
學(xué)習(xí)如何測(cè)試方法并處理模擬模塊的依賴關(guān)系营搅。
What should we test in methods? That’s a question that we had when we started doing unit tests. Everything comes down to test what that method do, and just that. This means we need to avoid calls to any dependency, so we’ll need to mock them.
哪些是我們?cè)趍ethods中應(yīng)該測(cè)試的呢?在我們剛著手單測(cè)的時(shí)候是個(gè)問題。其實(shí)測(cè)試方法無(wú)非就是測(cè)它做了什么。這意味著我們需要避免調(diào)用其它依賴,所以我們需要模擬出來(lái)朵逝。
Let’s add a onSubmit event to the form in the Form.vue component that we created in the last article:
我們?cè)谏衔闹袆?chuàng)建的Form組件中為表單添加一個(gè)提交事件:
...
<form action="" @submit.prevent="onSubmit(inputValue)">
...
The .prevent modifier is just a convenient way to call event.preventDefault() in order to don’t reload the page. Now make some modifications to call an api and store the result, by adding a results array to the data and a onSubmit method:
.prevent
是調(diào)用event.preventDefault()
的語(yǔ)法糖,為的就是不重載頁(yè)面。現(xiàn)在我們可以做些修改翔怎,在data中添加一個(gè)數(shù)組,methods中添加一個(gè)onSubmit方法來(lái)調(diào)用一個(gè)api接口,然后將返回結(jié)果賦給數(shù)組赤套。
data: () => ({
inputValue: '',
results: []
}),
methods: {
onSubmit(value) {
axios.get('https://jsonplaceholder.typicode.com/posts?q=' + value).then(results => {
this.results = results.data
})
}
},
...
The method is using axios to perform an HTTP call to the “posts” endpoint of jsonplaceholder, which is just a RESTful API for this kind of examples, and with the q query parameter we can search for posts, using the value provided as parameter.
onSubmit方法運(yùn)用了axios工具飘痛,來(lái)對(duì)https://jsonplaceholder.typicode.com/posts
進(jìn)行了一次HTTP請(qǐng)求,這只是個(gè)api的測(cè)試示例,通過url拼接參數(shù)容握,來(lái)取得對(duì)應(yīng)的返回結(jié)果宣脉。
For testing the onSubmit method:
對(duì)于測(cè)試onSubmit方法來(lái)說:
-
We don’t wanna call axios.get actual method
- 我們不想真正調(diào)用axios的get方法
- We wanna check it is calling axios (but not the real one) and it returns a promise
- 我們只是想驗(yàn)證onSubmit方法調(diào)用了axios,而且axios返回了一個(gè)promise對(duì)象剔氏。
-
That promise callback should set this.results to the promise result
- 這個(gè)promise的回調(diào)函數(shù)應(yīng)該將promise的返回結(jié)果賦值給
this.results
- This is probably one of the hardest things to test, when you have external dependencies plus those return promises that do things inside. What we need to do is to mock the external dependencies.
- 這次應(yīng)該是我們測(cè)試用例中最難的一個(gè)了塑猖,我們有外部環(huán)境依賴時(shí)可以依據(jù)能返回的promise對(duì)象們?cè)诮M件內(nèi)做很多事。我們現(xiàn)在需要做的就是要模擬這些外部依賴谈跛。
- 這個(gè)promise的回調(diào)函數(shù)應(yīng)該將promise的返回結(jié)果賦值給
Mock External Module Dependencies
模擬外部模塊的依賴
Jest provides a really great mocking system that allows you to mock everything in a quite convenient way. You don’t need any extra libraries for that. We have seen already jest.spyOn and jest.fn for spying and creating stub functions, although that’s not enough for this case.
Jest提供給我們一套超級(jí)棒的mock系統(tǒng)羊苟,可以讓我們輕松方便地模擬任何事物。從而不再需要引入其他類庫(kù)來(lái)做這種事情感憾。我們已經(jīng)見識(shí)到了運(yùn)jest.spyOn和jest.fn方法來(lái)監(jiān)測(cè)并創(chuàng)建stub函數(shù)践险,然而這些對(duì)我們的測(cè)試用例來(lái)所還不夠。
We need to mock the whole axios module. Here’s where jest.mock comes into the stage. It allow us to easily mock module dependencies by writing at the top of you file:
我們需要模擬整個(gè)axios依賴模塊吹菱。這里我們就能看到j(luò)est.mock大放光彩了巍虫!它可以讓我們輕易模擬依賴的模塊,只需要再文件頭部寫如下代碼:
jest.mock('dependency-path', implementationFunction)
You must know that jest.mock is hoisted, which means it will be placed at the top. So:
有一點(diǎn)要注意鳍刷,jest.mock需要寫在文件頂部占遥。
jest.mock('something', jest.fn)
import foo from 'bar'
...
Is equivalent to:
以上寫法等同于:
import foo from 'bar'
jest.mock('something', jest.fn) // this will end up above all imports and everything
...
By the date of writing, I still haven’t seen much info about how to do in Jest what we’re gonna do here on the internet. Lucky you don’t have to go through the same struggle.
行文時(shí)為止,我始終沒有見過網(wǎng)絡(luò)上有我們接下來(lái)要講的知識(shí)的相關(guān)文章输瓜。你們不必向我當(dāng)初那樣糾結(jié)了瓦胎。
Let’s write the mock for axios at the top of the Form.test.js test file, and the corresponding test case:
現(xiàn)在在文件頂部開始模擬axios模塊,并準(zhǔn)備相應(yīng)的用例:
jest.mock('axios', () => ({
get: jest.fn()
}))
import { shallow } from 'vue-test-utils'
import Form from '../src/components/Form'
import axios from 'axios' // axios here is the mock from above!
...
it('Calls axios.get', () => {
cmp.vm.onSubmit('an')
expect(axios.get).toBeCalledWith('https://jsonplaceholder.typicode.com/posts?q=an')
})
This is great, we’re indeed mocking axios, so the original axios is not called nor any HTTP call. And, we’re even checking by using toBeCalledWith that it’s been called with the right parameters. But we’re still missing something: we’re not checking that it returns a promise.
這樣太棒了尤揣,我們確實(shí)模擬了axios搔啊,所以真實(shí)的axios就不會(huì)被調(diào)用來(lái)發(fā)起HTTP請(qǐng)求。而且北戏,我們甚至用toBeCalledWith驗(yàn)證了axios會(huì)被傳入?yún)?shù)并調(diào)用负芋。但是我們?nèi)匀贿z漏了某些事情:我們沒有檢查返回的promise。
First we need to make our mocked axios.get method to return a promise. jest.fn accepts a factory function as a parameter, so we can use it to define its implementation:
首先嗜愈,我們需要我們模擬的axios.get方法來(lái)返回一個(gè)promise對(duì)象旧蛾,jest.fn接受一個(gè)工廠函數(shù)來(lái)作為參數(shù),使得我們可以用它來(lái)定義執(zhí)行條件:
jest.mock('axios', () => ({
get: jest.fn(() => Promise.resolve({ data: 3 }))
}))
But still, we cannot access the promise, because we’re not returning it. In testing, is a good practice to return something from a function when possible, it makes testing much easier. Let’s do it then in the onSubmit method of the Form.vue component:
但是如此還不行蠕嫁,我們依然拿不到promise對(duì)象锨天,因?yàn)槲覀儧]有返回它。在測(cè)試中剃毒,我們要盡量做到能讓一個(gè)函數(shù)返回?cái)?shù)據(jù)病袄,這樣可以讓測(cè)試更簡(jiǎn)單搂赋。接下來(lái)我們就實(shí)際運(yùn)用它:
onSubmit(value) {
const getPromise = axios.get('https://jsonplaceholder.typicode.com/posts?q=' + value)
getPromise.then(results => {
this.results = results.data
})
return getPromise
}
Then we can use the very clean ES2017 async/await syntax in the test to check the promise result:
這樣一來(lái)我們就可以用ES6中非常簡(jiǎn)潔的async/await、syntax方法益缠,在測(cè)試中來(lái)檢測(cè)promise的結(jié)果:
it('Calls axios.get and checks promise result', async () => {
const result = await cmp.vm.onSubmit('an')
expect(result).toEqual({ data: [3] })
expect(cmp.vm.results).toEqual([3])
expect(axios.get).toBeCalledWith('https://jsonplaceholder.typicode.com/posts?q=an')
})
You can see that we don’t only check the promise result, but also that the results internal state of the component is updated as expected, by doing expect(cmp.vm.results).toEqual([3]).
你可以看到我們不只是檢查了proise的結(jié)果厂镇,還驗(yàn)證了結(jié)果內(nèi)部的數(shù)據(jù)在組件中已經(jīng)被如期更新,正如斷言表達(dá)式expect(cmp.vm.results).toEqual([3]).
那樣左刽。
Keep mocks externalized
保證模擬的模塊被隔離
Jest allows us to have all our mocks separated in their own JavaScript file, placing them under a mocks folder, keeping the tests as clean as possible.
Jest允許我們模擬的依賴模塊與真實(shí)的模塊代碼隔離捺信,將其放置在mocks文件夾下,保證了依賴的清潔欠痴。
So we can take the jest.mock... block from top of the Form.test.js file out to it’s own file:
所以我們可以將 jest.mock... 相關(guān)模擬數(shù)據(jù)放在mocks內(nèi)axios.js中:
// test/__mocks__/axios.js
module.exports = {
get: jest.fn(() => Promise.resolve({ data: [3] }))
}
Just like this, with no extra effort, Jest automatically applies the mock in all our tests so we don’t have to do anything extra or mocking it in every test manually. Notice the module name must match the file name. If you run the tests again, they should still pass.
這樣依賴迄靠,不需要其他的額外模擬操作,Jest自動(dòng)地就為我們所有的測(cè)試應(yīng)用模擬數(shù)據(jù)喇辽,此后就不用每次手動(dòng)操作了掌挚。注意到我們的模塊名依然與文件名匹配。如果你執(zhí)行測(cè)試命令菩咨,它們都將完美通過測(cè)試吠式。
Keep in mind the modules registry and the mocks state is kept, so if you write another test afterwards, you may get undesired results:
要注意的是模擬的模塊和數(shù)據(jù)的狀態(tài)會(huì)一直保持,所以如果你接下來(lái)還要寫其他測(cè)試用例抽米,那么結(jié)果就不是你想要的了:
it('Calls axios.get', async () => {
const result = await cmp.vm.onSubmit('an')
expect(result).toEqual({ data: [3] })
expect(cmp.vm.results).toEqual([3])
expect(axios.get).toBeCalledWith('https://jsonplaceholder.typicode.com/posts?q=an')
})
it('Axios should not be called here', () => {
expect(axios.get).toBeCalledWith('https://jsonplaceholder.typicode.com/posts?q=an')
})
The second test should fail, but it doesn’t! That’s because axios.get was called on the test before.
第二個(gè)用例本來(lái)應(yīng)該報(bào)錯(cuò)的特占,卻被通過了測(cè)試。這是因?yàn)閍xios.get之前測(cè)試中被調(diào)用過了云茸。
For that reason, it’s a good practice to clean the module registry and the mocks, since they’re manipulated by Jest in order to make mocking happen. For that you can add in your beforeEach:
因?yàn)檫@個(gè)原因是目,我們直接清空被注冊(cè)的模擬模塊就好了,下面的代碼可以添加在beforeEach函數(shù)里标捺。
beforeEach(() => {
cmp = shallow(Form)
jest.resetModules()
jest.clearAllMocks()
})
That will ensure each test starts with clean mocks and modules, as it should be in unit testing.
這樣就可以確保每個(gè)測(cè)試用例都會(huì)在開始的時(shí)候有一個(gè)清潔無(wú)污染的模擬依賴環(huán)境懊纳。