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.
學(xué)習(xí)如何測試組件對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è)計(jì)的目的不同,一個(gè)是同步一個(gè)是異步,所以使得他們的行為迥異。
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功能是一個(gè)簡單的反應(yīng)式函數(shù),以另一種形式返回?cái)?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:
實(shí)際上,當(dāng)你創(chuàng)建基于Vue組件的Class類的時(shí)候,你可以就這么開發(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屬性還是有必要的,因?yàn)槲覀兿胫烙?jì)算屬性是否將input的內(nèi)容清空了,或成功綁定了數(shù)據(jù)。下面就是我們的測試方法:
First of all, create a Form.vue component:
首先,創(chuàng)建一個(gè)表單組件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.
這個(gè)表單中會(huì)顯示一個(gè)輸入框,緊接著會(huì)顯示反轉(zhuǎn)后的輸入內(nèi)容。這只是個(gè)簡單至極的例子,但是已經(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)建一個(gè)Form.test.js文件,只需要在文件中手動(dòng)引入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)在寫一個(gè)測試套件,來測以下兩個(gè)用例:
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.
我們可以用組件實(shí)例來訪問到cmp.vm對象,所以我們可以直接拿到組件的內(nèi)部state, computed properties 和 methods屬性。然后,為了測試組件,我們只需要改變以下輸入框的值,看返回的值是否符合我們的預(yù)期——反轉(zhuǎn)開關(guān)為false時(shí),字符串并沒有被反轉(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.
對于第二個(gè)用例,并沒有太大差別,只不過我們必須設(shè)置反轉(zhuǎn)開關(guān)為true。我們依然可以通過cmp.vm來改變其值,但是vue-test-utils還提供給我們一個(gè)更簡單實(shí)用的方法——setProps({ property: value, ... })
That’s it, depending on the computed property it may need more test cases.
以上用例比較簡單,但是基于computed的用例設(shè)計(jì)遠(yuǎn)不止于此。
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.
坦白講,我實(shí)際開發(fā)過程中,我沒有見過非得用watchers屬性,而不能用computed解決的問題。我也見了很多對watchers誤用的例子,簡直就是將工作流搞得一團(tuán)糟,所以在用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進(jìn)行測試
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添加一個(gè)監(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會(huì)將監(jiān)測方法名在屬性和組件的State的data中遍歷,就像此處的inputValue,因?yàn)榭梢栽赿ata中找到,接下來就會(huì)被列為監(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ù)中第一個(gè)是新值,舊值在第二處。我們當(dāng)前選擇只有在輸入框值不為空或者被改寫的時(shí)候才打印出值,通常,我們希望為每個(gè)用例編寫一個(gè)測試,當(dāng)然,這取決于你有余時(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進(jìn)行暗地窺測,在每一個(gè)測試之前先進(jìn)行初始化,測試完以后再進(jìn)行恢復(fù),這樣,每次測試都是一個(gè)干凈、無數(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
要測試一個(gè)監(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)用的,但是事實(shí)呢?我們可以看到運(yùn)行測試后并沒有得到預(yù)期效果。這是為什么呢?好吧,這是因?yàn)楦?jì)算屬性不同,監(jiān)控屬性被推遲在下一個(gè)vue尋找數(shù)據(jù)變化的更新周期中。所以,總的說來,console.log方法確實(shí)被調(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: 解決這個(gè)問題我們需要用到`vm.nextTick`方法,此方法可以推遲代碼到下一個(gè)更新周期,但是如果我們?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? 測試用例依然會(huì)被斷言失敗,這是因?yàn)闇y試結(jié)束后,斷言表達(dá)式依然沒有被調(diào)用。這類情況發(fā)生是因?yàn)槿缃翊a執(zhí)行在異步隊(duì)列中,會(huì)在回調(diào)`nextTick`中被執(zhí)行。我們怎么才能成功測試到后續(xù)才會(huì)執(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給我們傳了一個(gè)next參數(shù),我們可以用它在測試用例中測試回調(diào),原理就是如果next存在,那么測試就會(huì)將它之前的代碼完全執(zhí)行完畢,執(zhí)行next()后再結(jié)束測試。但是如果next不存在,那么測試會(huì)被同步執(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:
我們可以對其他兩個(gè)用例用同樣的方法,代碼略有不同:
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.
第二個(gè)用例其實(shí)遠(yuǎn)比表面的代碼要復(fù)雜。組件內(nèi)部數(shù)據(jù)的默認(rèn)值是空,所以首先我們需要改變它,等待下一個(gè)更新周期,然后清除模擬的mock、對調(diào)用復(fù)位,然后再改變它。之后的一個(gè)更新周期,我們就可以檢查窺測的對象并完成測試了。
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)建組件實(shí)例,改寫data屬性時(shí)會(huì)更簡便。請留心一件事——我們可以用mount或shallow函數(shù)的第二個(gè)參數(shù)修改任意組件選項(xiàng)。
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)學(xué)到了如何測試Vue組件的一部分邏輯代碼——計(jì)算屬性和監(jiān)測屬性。我們也盡量多地覆蓋測試面。希望您已經(jīng)學(xué)會(huì)了一些Vue的基礎(chǔ)知識,比如nextTick的更新周期。