Test Properties and Custom Events in Vue.js Components with Jest
運用Jest測試Vue組件中的繼承屬性和自定義事件
There are different ways to test properties, events and custom events.
測試Vue組件中的繼承屬性膏萧、事件和自定義事件有很多種方法枫耳。
Properties are custom attributes passed from parent to child components. Custom events solve just the opposite, they send data out to the direct parent via an event. They both combined are the wires of interaction and communication in Vue.js components.
繼承屬性是由父組件傳遞進子組件的屬性數(shù)據(jù)回还。自定義事件恰恰相反,是子組件通過一個事件將屬性數(shù)據(jù)傳遞給直接父組件。他們都將Vue的組件聯(lián)系起來歉闰,達到數(shù)據(jù)傳遞的橋梁作用犀被。
In Unit Testing, testing the in and outs (properties and custom events) means to test how a component behaves when it receives and sends out data in isolation. Let’s get our hands dirty!
在單元測試的范疇,測試數(shù)據(jù)接收與冒出(繼承屬性和自定義事件)意味著是測試組件在孤立的情況下寡壮,接收到和冒出數(shù)據(jù)時如何表現(xiàn)。
Properties
繼承屬性
When we are testing component properties, we can test how the component behave when we pass them certain properties. But before going on, an important note:
當我們測試組件的繼承屬性時讹弯,我們可以模擬給組件傳遞一個確定的數(shù)據(jù)屬性况既,測試組件的行為表現(xiàn)。開工之前组民,給您一個重要提示:
To pass properties to components, use propsData, and not props. The last one is to define properties, not to pass them data.
給組件傳遞數(shù)據(jù)棒仍,可以用propsData
,而不是正常組件中用的props
臭胜,后者是用來定義屬性的莫其,而不是用來給屬性傳遞數(shù)據(jù)癞尚。
First create a Message.test.js file and add the following code:
首先創(chuàng)建一個Message.test.js
文件,代碼如下:
describe('Message.test.js', () => {
let cmp
describe('Properties', () => {
// @TODO
})
})
We group test cases within a describe expression, and they can be nested. So we can use this strategy to group the tests for properties and events separately.
我們可以將測試用例歸集到一個describe
表達式里乱陡,且describe
還可以嵌套describe
浇揩。如此一來,我們就可以使用這個語法規(guī)則憨颠,將測試繼承屬性和自定義事件兩組用例分隔成組胳徽。
Then we’ll create a helper factory function to create a message component, give some properties
然后我們來創(chuàng)建一個函數(shù)來幫助我們按需生成組件實例,并且傳入一些屬性爽彤。
const createCmp = propsData => mount(Message, { propsData })
Testing property existence
測試屬性的存在與否
Two obvious things we can test is that a property exists, or it doesn’t. Remember that the Message.vue component has a message property, so let’s assert that it receives correctly that property. vue-test-utils comes with a hasProp(prop, value) function, which is very handy for this case:
很明顯养盗,我們是可以測試一個數(shù)據(jù)屬性存在與否。Message.vue
中有一個message屬性适篙,現(xiàn)在我們就可以斷言以下是否直接收到了傳遞進來的屬性值爪瓜。vue-test-utils
有一個api是hasProp(prop, value)
函數(shù),可以幫助我們方便進行檢測并斷言匙瘪。
it('has a message property', () => {
cmp = createCmp({ message: 'hey' })
expect(cmp.hasProp('message', 'hey')).toBeTruthy()
})
The properties behave in a way that they will be received only if they’re declared in the component. Meaning that if we pass a property that is not defined, it won’t be received. So to check for the no existence of a property, use a non-existing property:
一個屬性只有在組件中被聲明了铆铆,才能被接受到傳進的值,如果沒聲明丹喻,即使傳入薄货,那也接收不到。所以我們可以測試一下不存在是屬性是否收到了傳入的值:
it('has no cat property', () => {
cmp = createCmp({ cat: 'hey' })
expect(cmp.hasProp('cat', 'hey')).toBeFalsy()
})
However, in this case that test will fail because Vue has non-props attributes which sets it to the root of the Message component, thus being recognized as a prop and then the test will return true. Changing it to toBeTruty will make it pass for this example:
然而碍论,在這個用例中測試會失敗谅猾,因為Vue有non-props attributes
特性,這個特性會將props中沒聲明的屬性設置在根組件中鳍悠,所以檢測如果將message斷言為props并檢測其值税娜,那么案例就會被通過。所以這個用例中可以修改如下藏研,將成功通過測試敬矩。
it('has no cat property', () => {
cmp = createCmp({ cat: 'hey' });
expect(cmp.hasProp('cat', 'hey')).toBeTruthy()
})
We can test the default value as well. Go to Message.vue and change the props as follows:
我們還可以測試props中屬性的默認值,代碼作如下修改:
props: {
message: String,
author: {
type: String,
default: 'Paco'
}
},
Then the test could be:
然后測試用例可以如下面這樣寫:
it('Paco is the default author', () => {
cmp = createCmp({ message: 'hey' })
expect(cmp.hasProp('author', 'Paco')).toBeTruthy()
})
Asserting properties validation
對屬性的合法性進行檢測
Properties can have validation rules, ensuring that a property is required or it is of a determined type. Let’s write the message property as follows:
繼承屬性有一個合法性的規(guī)則蠢挡,能保證一個屬性是否是必備或符合既定規(guī)則』≡溃現(xiàn)在我們編輯message
的屬性規(guī)則如下:
props: {
message: {
type: String,
required: true,
validator: message => message.length > 1
}
}
Going further, you could use custom constructors types or custom validation rules, as you can see in the docs. Don’t do this right now, I’m just showing it as an example:
進一步的測試我們可以使用自定義構造函數(shù)的類型或自定義的驗證規(guī)則,設置屬性類型业踏。以下是例子:
class Message {}
...
props: {
message: {
type: Message, // It's compared using instance of
...
}
}
}
Whenever a validation rule is not fulfilled, Vue shows a console.error. For example, for createCmp({ message: 1 }), the next error will be shown:
無論何時禽炬,一個驗證規(guī)則如果不被滿足,Vue就會顯示一個打印錯誤勤家。下面是錯誤信息示例腹尖。
[Vue warn]: Invalid prop: type check failed for prop "message". Expected String, got Number.
(found in <Root>)
By the date of writing, vue-test-utils doesn’t have any utility to test this. We could use jest.spyOn to test it:
迄今為止,vue-test-utils
還沒有相關工具可以測試這個錯誤伐脖,我們可以用jest.spyOn
來搞定热幔。
it('message is of type string', () => {
let spy = jest.spyOn(console, 'error')
cmp = createCmp({ message: 1 })
expect(spy).toBeCalledWith(expect.stringContaining('[Vue warn]: Invalid prop'))
spy.mockReset() // or mockRestore() to completely remove the mock
// 或者用 mockRestore() 來完全移除mock數(shù)據(jù)
})
Here we’re spying on the console.error function, and checking that it shows a message containing a specific string. This is not an ideal way to check it, since we’re spying on global objects and relying on side effects.
上述代碼暗中監(jiān)視了console.error
函數(shù)乐设,檢測了顯示的信息中是否包含指定的字符串。這個不是一個理想的檢測方法断凶,因為我們用的這個方法監(jiān)視了一個全局對象伤提,并且對本想避免的副作用比較依賴巫俺。
Fortunately, there is an easier way to do it, which is by checking vm.options. Here’s where Vue stores the component options “expanded”. With expanded I mean: you can define your properties in a different ways: 幸運的是认烁,我們還有一個更簡單的方法 ——檢測`vm.options對象實現(xiàn)測試目的。
vm.$options`對象存儲著組件的擴展選項介汹。擴展選項的意思是你可以用一種另類的方式定義你的繼承屬性却嗡。
props: ['message']
// or
props: {
message: String
}
// or
props: {
message: {
type: String
}
}
But they all will end up in the most expanded object form (like the last one). So if we check the cmp.vm.option.props.message, for the first case, they all will be in the { type: X } format (although for the first example it will be { type: null}) 以上是三種定義類型的表達方式,但是他們在大部分擴展對象中都會已最后一種格式表示嘹承。我們索引到`cmp.vm.option.props.message時窗价,會發(fā)現(xiàn)他們都會被格式化為
{ type: X }的形式,不過第一種的結果是
{ type: null}`叹卷。
With this in mind, we could write a test suite to test that asserts that the message property has the expected validation rules:
如此一來撼港,所以我們就可以寫個測試系列案例來斷言信息的屬性是否符合預期驗證規(guī)則。
describe('Message.test.js', () => {
// ...
describe('Properties', () => {
// ...
describe('Validation', () => {
const message = createCmp().vm.$options.props.message
it('message is of type string', () => {
expect(message.type).toBe(String)
})
it('message is required', () => {
expect(message.required).toBeTruthy()
})
it('message has at least length 2', () => {
expect(message.validator && message.validator('a')).toBeFalsy()
expect(message.validator && message.validator('aa')).toBeTruthy()
})
})
})
})
Custom Events
自定義事件
We can test at least two things in Custom Events:
我們至少可以測試自定義事件的兩類情況:
- Asserting that after an action an event gets triggered
- 在一個action后斷言事件被觸發(fā)
- Checking what an event listener calls when it gets triggered
- 檢測事件被觸發(fā)時調用了什么方法
Which in the case of the MessageList.vue and Message.vue components example, that gets translated to:
對于MessageList.vue
和Message.vue
兩個組件骤竹,以上兩類相當于:
-
Assert that Message components triggers a message-clicked when a message gets clicked
- 對Message組件斷言:如果一項message被點擊帝牡,會觸發(fā)message-clicked事件
-
Check in MessageList that when a message-clicked happens, a handleMessageClick function is called
- 檢測MessageList組件中,當一項message被點擊蒙揣,
handleMessageClick
方法會被調用
- 檢測MessageList組件中,當一項message被點擊蒙揣,
First, go to Message.vue and use emit to trigger that custom event: 首先我們在Message組件中用emit來觸發(fā)一個自定義事件
<template>
<li
style="margin-top: 10px"
class="message"
@click="handleClick">
{{message}}
</li>
</template>
<script>
export default {
name: 'Message',
props: ['message'],
methods: {
handleClick() {
this.$emit('message-clicked', this.message)
}
}
}
</script>
And in MessageList.vue, handle the event using @message-clicked:
在父組件MessageList.vue中靶溜,我們用@message-clicked
綁定handleMessageClick
方法,完成閉環(huán)
<template>
<ul>
<Message
@message-clicked="handleMessageClick"
:message="message"
v-for="message in messages"
:key="message"/>
</ul>
</template>
<script>
import Message from './Message'
export default {
name: 'MessageList',
props: ['messages'],
methods: {
handleMessageClick(message) {
console.log(message)
}
},
components: {
Message
}
}
</script>
Now it’s time to write a unit test. Create a nested describe within the test/Message.spec.js file and prepare the barebones of the test case “Assert that Message components triggers a message-clicked when a message gets clicked” that we mentioned before:
現(xiàn)在已經(jīng)萬事俱備止潮,可以寫單元測試了点把。在test/Message.spec.js
文件中創(chuàng)建一個嵌套describe語句拼缝,并為之前提到的Assert that Message components triggers a message-clicked when a message gets clicked
這個測試用例準備準系統(tǒng):
// ...
describe('Message.test.js', () => {
// ...
describe('Events', () => {
beforeEach(() => {
cmp = createCmp({ message: 'Cat' })
})
it('calls handleClick when click on message', () => {
// @TODO
})
})
})
Testing the Event Click calls a method handler
測試點擊事件調用的處理方法
The first thing we can test is that when clicking a message, the handleClick function gets called. For that we can use a trigger of the wrapper component, and a jest spy using spyOn function:
首先,我們先測試點擊單項信息后瓷炮,處理方法被調用。此處我們把jest的spyOn方法賦給message的容器組件來監(jiān)測觸發(fā)動作:
it('calls handleClick when click on message', () => {
const spy = spyOn(cmp.vm, 'handleClick')
cmp.update() // Forces to re-render, applying changes on template
const el = cmp.find('.message').trigger('click')
expect(cmp.vm.handleClick).toBeCalled()
})
See the cmp.update()? When we change things that are used in the template, handleClick in this case, and we want the template to apply the changes, we need to use the update function.
注意到cmp.update()
方法了嗎递宅?當模板內容有改動時崭别,即處理方法成功被調用后,我們想讓模板內容按預期有所改變恐锣,我們需要用cmp.update()
方法茅主。
Keep in mind that by using a spy the original method handleClick will be called. Probably you intentionally want that, but normally we want to avoid it and just check that on click the methods is indeed called. For that we can use a Jest Mock function:
要注意的是,我們預期只要檢測到處理方法被成功調用就ok了土榴,可是通常我們卻是要避免如此籠統(tǒng)的檢測诀姚,而是具體地檢查方法是否被直接調用。如此一來玷禽,我們需要用到Jest的Mock函數(shù):
it('calls handleClick when click on message', () => {
cmp.vm.handleClick = jest.fn()
cmp.update()
const el = cmp.find('.message').trigger('click')
expect(cmp.vm.handleClick).toBeCalled()
})
Here we’re totally replacing the handleClick method, accessible on the vm of the wrapper component returned by the mount function.
現(xiàn)在我們將處理方法替換掉赫段,改為檢測掛載方法返回的父組件實例上的vm對象呀打。
We can make it even easier by using setMethods helper that the official tools provide us:
甚至我們再簡單一些,直接用官方工具中的setMethods
函數(shù):
it('calls handleClick when click on message', () => {
const stub = jest.fn()
cmp.setMethods({ handleClick: stub })
const el = cmp.find('.message').trigger('click')
expect(stub).toBeCalled()
})
Using setMethods is the suggested way to do it, since is an abstraction that official tools give us in case the Vue internals change.
這里推薦使用setMethods
方法做這類測試糯笙,因為這是官方工具提供給我們的抽象方法贬丛,就是用來測試Vue組件內部數(shù)據(jù)變化的方法。
Testing the Custom Event message-clicked is emitted
測試message-clicked自定義事件被分發(fā)
We’ve tested that the click method calls it’s handler, but we haven’t tested that the handler emits the message-clicked event itself. We can call directly the handleClick method, and use a Jest Mock function in combination with the Vue vm on method: 我們已經(jīng)測試了點擊事件后處理方法被調用给涕,但仍然沒有測到`message-clicked`被處理方法分發(fā)后的狀況豺憔。我們可以直接調用處理方法,然后用jest的Mock方法結合Vue中vm的on方法來測試够庙。
it('triggers a message-clicked event when a handleClick method is called', () => {
const stub = jest.fn()
cmp.vm.$on('message-clicked', stub)
cmp.vm.handleClick()
expect(stub).toBeCalledWith('Cat')
})
See that here we’re using toBeCalledWith so we can assert exactly which parameters we expect, making the test even more robust. Not that we’re not using cmp.update() here, since we’re making no changes that need to propagate to the template.
可以看到我們運用toBeCalledWith
方法恭应,精確斷言了我們期望的參數(shù),使得用例代碼十分簡潔清爽耘眨。之所以不用cmp.update()
方法昼榛,是因為我們沒有做什么需要模板重新渲染的數(shù)據(jù)變更。
Testing the @message-clicked triggers an event
測試@message-clicked觸發(fā)的事件
For custom events, we cannot use the trigger method, since it’s just for DOM events. But, we can emit the event ourselves, by getting the Message component and using its vm.emit method. So add the following test to MessageList.test.js: 對于自定義事件剔难,我們不能用觸發(fā)事件的方法進行測試胆屿,因為那是基于DOM的測試方法。但是偶宫,我們可以通過Message組件的`vm.emit`方法分發(fā)事件非迹。代碼更改如下:
it('Calls handleMessageClick when @message-click happens', () => {
const stub = jest.fn()
cmp.setMethods({ handleMessageClick: stub })
cmp.update()
const el = cmp.find(Message).vm.$emit('message-clicked', 'cat')
expect(stub).toBeCalledWith('cat')
})
I’ll leave up to you to test what handleMessageClicked does.
對handleMessageClicked的測試就不再一一贅述了。