Part 6: 用Jest測(cè)試Vue中的Methods中的方法和Mock依賴

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)在需要做的就是要模擬這些外部依賴谈跛。

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)境懊纳。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市亡容,隨后出現(xiàn)的幾起案子嗤疯,更是在濱河造成了極大的恐慌,老刑警劉巖闺兢,帶你破解...
    沈念sama閱讀 206,214評(píng)論 6 481
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件茂缚,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡列敲,警方通過查閱死者的電腦和手機(jī)阱佛,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 88,307評(píng)論 2 382
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)戴而,“玉大人,你說我怎么就攤上這事翩蘸∷猓” “怎么了?”我有些...
    開封第一講書人閱讀 152,543評(píng)論 0 341
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)扶踊。 經(jīng)常有香客問我泄鹏,道長(zhǎng),這世上最難降的妖魔是什么秧耗? 我笑而不...
    開封第一講書人閱讀 55,221評(píng)論 1 279
  • 正文 為了忘掉前任备籽,我火速辦了婚禮,結(jié)果婚禮上分井,老公的妹妹穿的比我還像新娘车猬。我一直安慰自己,他們只是感情好尺锚,可當(dāng)我...
    茶點(diǎn)故事閱讀 64,224評(píng)論 5 371
  • 文/花漫 我一把揭開白布珠闰。 她就那樣靜靜地躺著,像睡著了一般瘫辩。 火紅的嫁衣襯著肌膚如雪伏嗜。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,007評(píng)論 1 284
  • 那天伐厌,我揣著相機(jī)與錄音承绸,去河邊找鬼。 笑死挣轨,一個(gè)胖子當(dāng)著我的面吹牛八酒,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播刃唐,決...
    沈念sama閱讀 38,313評(píng)論 3 399
  • 文/蒼蘭香墨 我猛地睜開眼羞迷,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了画饥?” 一聲冷哼從身側(cè)響起衔瓮,我...
    開封第一講書人閱讀 36,956評(píng)論 0 259
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎抖甘,沒想到半個(gè)月后热鞍,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 43,441評(píng)論 1 300
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡衔彻,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 35,925評(píng)論 2 323
  • 正文 我和宋清朗相戀三年薇宠,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片艰额。...
    茶點(diǎn)故事閱讀 38,018評(píng)論 1 333
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡澄港,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出柄沮,到底是詐尸還是另有隱情回梧,我是刑警寧澤废岂,帶...
    沈念sama閱讀 33,685評(píng)論 4 322
  • 正文 年R本政府宣布,位于F島的核電站狱意,受9級(jí)特大地震影響湖苞,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜详囤,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 39,234評(píng)論 3 307
  • 文/蒙蒙 一财骨、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧藏姐,春花似錦隆箩、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,240評(píng)論 0 19
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至问畅,卻和暖如春娃属,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背护姆。 一陣腳步聲響...
    開封第一講書人閱讀 31,464評(píng)論 1 261
  • 我被黑心中介騙來(lái)泰國(guó)打工矾端, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人卵皂。 一個(gè)月前我還...
    沈念sama閱讀 45,467評(píng)論 2 352
  • 正文 我出身青樓秩铆,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親灯变。 傳聞我的和親對(duì)象是個(gè)殘疾皇子殴玛,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 42,762評(píng)論 2 345

推薦閱讀更多精彩內(nèi)容