Test Computed Properties and Watchers in Vue.js Components with Jest
測試Vue組件的Computed和Watchers功能
Learn about testing Computed Properties and Watchers reactivity in Vue.js.
學習如何測試組件對Computed和Watchers功能的反應(yīng)氯哮。
Computed properties and watchers are reactive parts of the logic of Vue.js components. They both serve totally different purposes, one is synchronous and the other asynchronous, which makes them behave slightly different.
Computed和Watchers功能是vue.js組件的邏輯反應(yīng)部分棚亩。他們被設(shè)計的目的不同婴渡,一個是同步一個是異步,所以使得他們的行為迥異。
In this article we’ll go through testing them and see what different cases we can find on the way.
這篇文章中我們通過測試Computed和Watchers功能立磁,來發(fā)現(xiàn)它們彼此的特性。
Computed Properties
Computed功能
Computed properties are simple reactive functions that return data in another form. They behave exactly like the language standard get/set properties:
Computed功能是一個簡單的反應(yīng)式函數(shù),以另一種形式返回數(shù)據(jù)佑吝。它們的行為就像編程語言中常見的get或set方法。
class X {
...
get fullName() {
return `${this.name} ${this.surname}`
}
set fullName() {
...
}
}
In fact, when you’re building class based Vue components, as I explain in my Egghead course “Use TypeScript to Develop Vue.js Web Applications”, you’ll write it just like that. If you’re using plain objects, it’d be:
實際上绳匀,當你創(chuàng)建基于Vue組件的Class類的時候芋忿,你可以就這么開發(fā)代碼炸客。如果你使用普通對象方式編輯代碼,那么可以如下開發(fā):
export default {
...
computed: {
fullName() {
return `${this.name} ${this.surname}`
}
}
}
And you can even add the set as follows:
還可以繼續(xù)添加set方法:
computed: {
fullName: {
get() {
return `${this.name} ${this.surname}`
},
set() {
...
}
}
}
Testing Computed Properties
測試Computed屬性
Testing a computed property is very simple, and probably sometimes you don’t test a computed property exclusively, but test it as part of other tests. But most times it’s good to have a test for it, whether that computed property is cleaning up an input, or combining data, we wanna make sure things work as intended. So let’s begin.
想測試Computed屬性非常簡單戈钢,甚至可能你根本不用專門來測它痹仙,只需要在其他測試用例中帶上它一起測了。但是通常測試Computed屬性還是有必要的殉了,因為我們想知道計算屬性是否將input的內(nèi)容清空了开仰,或成功綁定了數(shù)據(jù)。下面就是我們的測試方法:
First of all, create a Form.vue component:
首先宣渗,創(chuàng)建一個表單組件Form.vue:
<template>
<div>
<form action="">
<input type="text" v-model="inputValue">
<span class="reversed">{{ reversedInput }}</span>
</form>
</div>
</template>
<script>
export default {
props: ['reversed'],
data: () => ({
inputValue: ''
}),
computed: {
reversedInput() {
return this.reversed ?
this.inputValue.split("").reverse().join("") :
this.inputValue
}
}
}
</script>
It will show an input, and next to it the same string but reversed. It’s just a silly example, but enough to test it.
這個表單中會顯示一個輸入框抖所,緊接著會顯示反轉(zhuǎn)后的輸入內(nèi)容。這只是個簡單至極的例子痕囱,但是已經(jīng)足夠應(yīng)付此類的測試了田轧。
Now add it to App.vue, put it after the MessageList component, and remember to import it and include it within the components component option. Then, create a test/Form.test.js with the usual bare-bones we’ve used in other tests:
現(xiàn)在將此組件添加到App根組件,放到MessageList組件后鞍恢,別忘了引入并在components屬性中注冊為子組件傻粘。然后在test目錄下創(chuàng)建一個Form.test.js
文件,只需要在文件中手動引入Form組件帮掉。
import { shallow } from 'vue-test-utils'
import Form from '../src/components/Form'
describe('Form.test.js', () => {
let cmp
beforeEach(() => {
cmp = shallow(Form)
})
})
Now create a test suite with 2 test cases:
現(xiàn)在寫一個測試套件弦悉,來測以下兩個用例:
describe('Properties', () => {
it('returns the string in normal order if reversed property is not true', () => {
cmp.vm.inputValue = 'Yoo'
expect(cmp.vm.reversedInput).toBe('Yoo')
})
it('returns the reversed string if reversed property is true', () => {
cmp.vm.inputValue = 'Yoo'
cmp.setProps({ reversed: true })
expect(cmp.vm.reversedInput).toBe('ooY')
})
})
We can access the component instance within cmp.vm, so we can access the internal state, computed properties and methods. Then, to test it is just about changing the value and making sure it returns the same string when reversed is false.
我們可以用組件實例來訪問到cmp.vm
對象,所以我們可以直接拿到組件的內(nèi)部state, computed properties 和 methods屬性蟆炊。然后稽莉,為了測試組件,我們只需要改變以下輸入框的值涩搓,看返回的值是否符合我們的預(yù)期——反轉(zhuǎn)開關(guān)為false時污秆,字符串并沒有被反轉(zhuǎn)。
For the second case, it would be almost the same, with the difference that we must set the reversed property to true. We could navigate through cmp.vm... to change it, but vue-test-utils give us a helper method setProps({ property: value, ... }) that makes it very easy.
對于第二個用例昧甘,并沒有太大差別良拼,只不過我們必須設(shè)置反轉(zhuǎn)開關(guān)為true。我們依然可以通過cmp.vm
來改變其值充边,但是vue-test-utils
還提供給我們一個更簡單實用的方法——setProps({ property: value, ... })
That’s it, depending on the computed property it may need more test cases.
以上用例比較簡單庸推,但是基于computed的用例設(shè)計遠不止于此。
Watchers
Watchers
Honestly, I haven’t come across any case where I really need to use watchers that I computed properties couldn’t solve. I’ve seen them misused as well, leading to a very unclear data workflow among components and messing everything up, so don’t rush on using them and think beforehand.
坦白講浇冰,我實際開發(fā)過程中贬媒,我沒有見過非得用watchers屬性,而不能用computed解決的問題肘习。我也見了很多對watchers誤用的例子际乘,簡直就是將工作流搞得一團糟,所以在用watchers屬性之前一定要考慮再三井厌。
As you can see in the Vue.js docs, watchers are often used to react to data changes and perform asynchronous operations, such can be performing an ajax request.
正如你看到Vue的官方文檔中說的那樣蚓庭,watchers通常用于對于數(shù)據(jù)變更后做出響應(yīng),并執(zhí)行異步操作仅仆,如此一來就可以執(zhí)行Ajax請求了器赞。
Testing Watchers
對Watcher進行測試
Let’s say we wanna do something when the inputValue from the state change. We could do an ajax request, but since that’s more complicated and we’ll see it in the next lesson, let’s just do a console.log. Add a watch property to the Form.vue component options:
我們?nèi)绻朐诒韱沃?code>inputValue數(shù)據(jù)變化后做些什么,我們可以執(zhí)行一次Ajax請求墓拜,但是這太復(fù)雜了港柜,后文再說,現(xiàn)在只做下console.log
就可以了咳榜。對Form.vue
添加一個監(jiān)測屬性:
watch: {
inputValue(newVal, oldVal) {
if(newVal.trim().length && newVal !== oldVal) {
console.log(newVal)
}
}
}
Notice the inputValue watch function matches the state variable name. By convention, Vue will look it up in both properties and data state by using the watch function name, in this case inputValue, and since it will find it in data, it will add the watcher there.
有沒有注意到檢測的函數(shù)名字是inputValue夏醉,正好是組件state數(shù)據(jù)中的變量名?按照慣例涌韩,Vue會將監(jiān)測方法名在屬性和組件的State的data中遍歷畔柔,就像此處的inputValue,因為可以在data中找到臣樱,接下來就會被列為監(jiān)測對象靶擦。
See that a watch function takes the new value as a first parameter, and the old one as the second. In this case we’ve chosen to log only when it’s not empty and the values are different. Usually, we’d like to write a test for each case, depending on the time you have and how critical that code is.
在監(jiān)測函數(shù)的參數(shù)中第一個是新值,舊值在第二處雇毫。我們當前選擇只有在輸入框值不為空或者被改寫的時候才打印出值玄捕,通常,我們希望為每個用例編寫一個測試棚放,當然枚粘,這取決于你有余時以及待測試的代碼有多關(guān)鍵。
What should we test about the watch function? Well, that’s something we’ll also discuss further in the next lesson when we talk about testing methods, but let’s say we just wanna know that it calls the console.log when it should. So, let’s add the bare bones of the watchers test suite, within Form.test.js:
那么關(guān)于Watch的功能飘蚯,我們可以測哪些方面呢馍迄?這也留在后文,與methods屬性一起講⌒⒚埃現(xiàn)在我們只是想知道它符合我們的預(yù)期——調(diào)用了console.log
方法柬姚。所以,我們添加如下代碼:
describe('Form.test.js', () => {
let cmp
...
describe('Watchers - inputValue', () => {
let spy
beforeAll(() => {
spy = jest.spyOn(console, 'log')
})
afterEach(() => {
spy.mockClear()
})
it('is not called if value is empty (trimmed)', () => {
})
it('is not called if values are the same', () => {
})
it('is called with the new value in other cases', () => {
})
})
})
We’re using a spy on the console.log method, initializing before starting any test, and resetting its state after each of them, so that they start from a clean spy.
我們對console.log進行暗地窺測庄涡,在每一個測試之前先進行初始化量承,測試完以后再進行恢復(fù),這樣穴店,每次測試都是一個干凈撕捍、無數(shù)據(jù)污染的spy。
To test a watch function, we just need to change the value of what’s being watch, in this case the inputValue state. But there is something curious… let’s start by the last test
要測試一個監(jiān)測方法泣洞,我們只需要更改一下我們監(jiān)控下的數(shù)據(jù)值忧风,即此處的inputValue變量。但是現(xiàn)在有一件稀奇事:
it('is called with the new value in other cases', () => {
cmp.vm.inputValue = 'foo'
expect(spy).toBeCalled()
})
We change the inputValue, so the console.log spy should be called, right? Well, if you run it, you’ll notice that is not! WTF??? Wait, there is an explanation for this: unlike computed properties, watchers are deferred to the next update cycle that Vue uses to look for changes. So, basically, what’s happening here is that console.log is indeed called, but after the test has finished.
我們改變了inputValue球凰,按理監(jiān)測的console.log是應(yīng)該被調(diào)用的狮腿,但是事實呢腿宰?我們可以看到運行測試后并沒有得到預(yù)期效果。這是為什么呢缘厢?好吧吃度,這是因為跟計算屬性不同,監(jiān)控屬性被推遲在下一個vue尋找數(shù)據(jù)變化的更新周期中贴硫。所以椿每,總的說來,console.log方法確實被調(diào)用了英遭,但是被推遲到了測試代碼結(jié)束后间护。
To solve this, we need to use the vm.nextTick function to defer code to the next update cycle. But if we write: 解決這個問題我們需要用到`vm.nextTick`方法,此方法可以推遲代碼到下一個更新周期挖诸,但是如果我們?nèi)缦滤鶎懀?/p>
it('is called with the new value in other cases', () => {
cmp.vm.inputValue = 'foo'
cmp.vm.$nextTick(() => {
expect(spy).toBeCalled()
})
})
It will still fail, since the test finishes with the expect function not being called. That happens because now is asynchronous and happens on the nextTick callback. How can we then test it if the expect happens at a later time? 測試用例依然會被斷言失敗汁尺,這是因為測試結(jié)束后,斷言表達式依然沒有被調(diào)用多律。這類情況發(fā)生是因為如今代碼執(zhí)行在異步隊列中均函,會在回調(diào)`nextTick`中被執(zhí)行。我們怎么才能成功測試到后續(xù)才會執(zhí)行的代碼呢菱涤?
Jest give us a next parameter that we can use in the it test callbacks, in a way that if it is present, the test will not finish until next is called, but if it’s not, it will finish synchronously. So, to finally get it right:
Jest給我們傳了一個next參數(shù)苞也,我們可以用它在測試用例中測試回調(diào),原理就是如果next
存在粘秆,那么測試就會將它之前的代碼完全執(zhí)行完畢如迟,執(zhí)行next()
后再結(jié)束測試。但是如果next
不存在攻走,那么測試會被同步執(zhí)行殷勘。終于,我們?nèi)缭敢詢斄耍?/p>
it('is called with the new value in other cases', next => {
cmp.vm.inputValue = 'foo'
cmp.vm.$nextTick(() => {
expect(spy).toBeCalled()
next()
})
})
We can apply the same strategy for the other two, with the difference that the spy shouldn’t be called:
我們可以對其他兩個用例用同樣的方法昔搂,代碼略有不同:
it('is not called if value is empty (trimmed)', next => {
cmp.vm.inputValue = ' '
cmp.vm.$nextTick(() => {
expect(spy).not.toBeCalled()
next()
})
})
it('is not called if values are the same', next => {
cmp.vm.inputValue = 'foo'
cmp.vm.$nextTick(() => {
spy.mockClear()
cmp.vm.inputValue = 'foo'
cmp.vm.$nextTick(() => {
expect(spy).not.toBeCalled()
next()
})
})
})
That second one gets a bit more complex than it looked like. The default internal state is empty, so first we need to change it, wait for the next tick, then clear the mock to reset the call count, and change it again. Then after the second tick, we can check the spy and finish the test.
第二個用例其實遠比表面的代碼要復(fù)雜玲销。組件內(nèi)部數(shù)據(jù)的默認值是空,所以首先我們需要改變它摘符,等待下一個更新周期贤斜,然后清除模擬的mock、對調(diào)用復(fù)位逛裤,然后再改變它瘩绒。之后的一個更新周期,我們就可以檢查窺測的對象并完成測試了带族。
This can get simpler if we recreate the component at the beginning, overriding the data property. Remember we can override any component option by using the second parameter of the mount or shallow functions:
這樣如果我們在開頭部分重復(fù)創(chuàng)建組件實例锁荔,改寫data屬性時會更簡便。請留心一件事——我們可以用mount或shallow函數(shù)的第二個參數(shù)修改任意組件選項蝙砌。
it('is not called if values are the same', next => {
cmp = shallow(Form, { data: ({ inputValue: 'foo' }) })
cmp.vm.inputValue = 'foo'
cmp.vm.$nextTick(() => {
expect(spy).not.toBeCalled()
next()
})
})
Conclusion
總結(jié)
You’ve learned in this article how to test part of the logic of Vue components: computed properties and watchers. We’ve gone through different test cases we can come across testing them. Probably you’ve also learned some of the Vue internals such as the nextTick update cycles.
大家在此文中已經(jīng)學到了如何測試Vue組件的一部分邏輯代碼——計算屬性和監(jiān)測屬性阳堕。我們也盡量多地覆蓋測試面跋理。希望您已經(jīng)學會了一些Vue的基礎(chǔ)知識,比如nextTick的更新周期恬总。