Vue Test Utils
Guides
测试组件,首先需要用 mount 加载组件,这是在 Vue-Test-Utils 中渲染组件的主要方式。mount 组件后会返回一个 Wrapper , 它会暴露一系列和组件查询、交互的方法。
然后用get()、 find()、findAll()定位元素的位置,然后进行一系列的操作。
通过id,class,attr 获取元素,设置input的值等
wrapper 中的 get()
方法用来找已经存在的元素,用的是 querySelector
语法,如果没有获取到元素,会抛出一个错误。获取到元素会返回一个 DOMWrapper ,它是一个简洁版的 Wrapper 。
test('', async () => {
const wrapper = mount(Component) // 加载组件
// Vue是异步更新DOM的。我们需要将测试标记为async,并在可能导致DOM更改的任法上调用 await。
await wrapper.get('.input').setValue('hello') // 通过class获取DOM, setValue设置input值
await wrapper.get('#input').setValue('hello') // 通过id获取DOM
await wrapper.get('input').setValue('hello') // 通过tag获取DOM
// 使用 trigger 提交form、button
await wrapper.get('#form').trigger('submit')
// text() 获取DOM文本
expect( wrapper.get('#content').text()).toEqual('Hello')
// 通过属性获取DOM, toContain 是否包含某值
expect(wrapper.get('[data-name="heaedr"]').classes()).toContain('active')
// toHaveLength 判断元素的个数
expect(wrapper.findAll('[class="box"]')).toHaveLength(2)
})
get
在假定元素存在的时候起作用,当不存在的时候会报错,并终止test。一般不建议用 get 来断言是否存在。建议使用 find()
和 exists()
。
find
和 mount 比较像,返回一个 Wrapper 。
test('', async () => {
// exists 判断是否存在
expect(wrapper.find('#admin').exists()).toBe(false)
})
给组件传值
test('renders an admin link', () => {
const wrapper = mount(Nav, {
data() {
return {
admin: true
}
}
})
expect(wrapper.get('#admin').text()).toEqual('Admin')
})
判断组件的显隐
/**
* 组件 v-show = false, 则 find 和 exists 会返回true, 因为元素始终在DOM中
* 应该使用 isVisible()
* display: none, visibility: hidden, opacity :0
* <details> 中的标签
* hidden 属性
* 以上情况 isVisible() 返回 false.
* /
expect(wrapper.get('#user-dropdown').isVisible()).toBe(false)
触发事件
const Counter = {
template: '<button @click="handleClick">Increment</button>',
data() {
return {
count: 0
}
},
methods: {
handleClick() {
this.count += 1
this.$emit('increment', this.count)
}
}
}
test('emits an event when clicked', () => {
const wrapper = mount(Counter)
wrapper.find('button').trigger('click')
wrapper.find('button').trigger('click')
// emitted() 它返回一个包含组件发出的所有事件的对象
expect(wrapper.emitted()).toHaveProperty('increment')
const incrementEvent = wrapper.emitted('increment')
// 触发了两次”click“事件,因此 ’increment‘事件数组长度应该为 2
expect(incrementEvent).toHaveLength(2)
// 第一次点击
// 注意值应该是个数组
expect(incrementEvent[0]).toEqual([1])
// 第二次点击
expect(incrementEvent[1]).toEqual([2])
// console.log(wrapper.emitted('increment'))
[
[1], // 第一次点击 `count` is 1
[2] // 第二次点击 `count` is 2
]
})
复杂事件
const Counter = {
template: `<button @click="handleClick">Increment</button>`,
data() {
return {
count: 0
}
},
methods: {
handleClick() {
this.count += 1
this.$emit('increment', {
count: this.count,
isEven: this.count % 2 === 0
})
}
}
}
test('emits an event with count when clicked', () => {
const wrapper = mount(Counter)
wrapper.find('button').trigger('click')
wrapper.find('button').trigger('click')
expect(wrapper.emitted('increment')[0]).toEqual([
{
count: 1,
isEven: false
}
])
expect(wrapper.emitted('increment')[1]).toEqual([
{
count: 2,
isEven: true
}
])
})
测试Form
这里常用的方法主要是
setValue
和trigger
<template>
<div>
<input type="email" v-model="email" />
<button @click="submit">Submit</button>
</div>
</template>
<script>
export default {
data() {
return {
email: ''
}
},
methods: {
submit() {
this.$emit('submit', this.email)
}
}
}
</script>
test('sets the value', async () => {
const wrapper = mount(Component)
const input = wrapper.find('input')
// 如果你没有为OPTION, CHECKBOX或RADIO输入传递一个参数给setValue,它们将被设置为选中。
// 当使用 setValue 时, 使用 await 确保 vue 可以对行为做出反应
await input.setValue('my@mail.com')
expect(input.element.value).toBe('my@mail.com')
await wrapper.find('button').trigger('click')
expect(wrapper.emitted()).toHaveProperty('submit')
})
复杂 form 测试演示
<template>
<form @submit.prevent="submit">
<input type="email" v-model="form.email" />
<textarea v-model="form.description" />
<select v-model="form.city">
<option value="new-york">New York</option>
<option value="moscow">Moscow</option>
</select>
<input type="checkbox" v-model="form.subscribe" />
<input type="radio" value="weekly" v-model="form.interval" />
<input type="radio" value="monthly" v-model="form.interval" />
<button type="submit">Submit</button>
</form>
</template>
<script>
export default {
data() {
return {
form: {
email: '',
description: '',
city: '',
subscribe: false,
interval: ''
}
}
},
methods: {
async submit() {
this.$emit('submit', this.form)
}
}
}
</script>
import { mount } from '@vue/test-utils'
import FormComponent from './FormComponent.vue'
test('submits a form', async () => {
const wrapper = mount(FormComponent)
const email = 'name@mail.com'
const description = 'Lorem ipsum dolor sit amet'
const city = 'moscow'
await wrapper.find('input[type=email]').setValue(email)
await wrapper.find('textarea').setValue(description)
await wrapper.find('select').setValue(city)
await wrapper.find('input[type=checkbox]').setValue()
await wrapper.find('input[type=radio][value=monthly]').setValue()
await wrapper.find('form').trigger('submit.prevent')
expect(wrapper.emitted('submit')[0][0]).toStrictEqual({
email,
description,
city,
subscribe: true,
interval: 'monthly'
})
})
测试复杂 Input 组件
项目中可能会使用到 UI 库,参照下面的例子
<template>
<form @submit.prevent="handleSubmit">
<v-textarea v-model="description" ref="description" />
<button type="submit">Send</button>
</form>
</template>
<script>
export default {
name: 'CustomTextarea',
data() {
return {
description: ''
}
},
methods: {
handleSubmit() {
this.$emit('submitted', this.description)
}
}
}
</script>
test('emits textarea value on submit', async () => {
const wrapper = mount(CustomTextarea)
const description = 'Some very long text...'
// findComponent
await wrapper.findComponent({ ref: 'description' }).setValue(description)
wrapper.find('form').trigger('submit')
expect(wrapper.emitted('submitted')[0][0]).toEqual(description)
})
向组件传递数据
const Password = {
template: `
<div>
<input v-model="password">
<div v-if="error">{{ error }}</div>
</div>
`,
props: {
minLength: {
type: Number
}
},
computed: {
error() {
if (this.password.length < this.minLength) {
return `Password must be at least ${this.minLength} characters.`
}
return
}
}
}
test('renders an error if length is too short', () => {
const wrapper = mount(Password, {
props: {
minLength: 10
},
data() {
return {
password: 'short'
}
}
})
// await wrapper.setProps({ minLength: 10 })
// await wrapper.setData({ count: 2 })
expect(wrapper.html()).toContain('Password must be at least 10 characters')
})
slot
const Layout = {
template: `
<div>
<h1>Welcome!</h1>
<header>
<slot name="header" />
</header>
<main>
<slot />
</main>
<footer>
<slot name="footer" />
</footer>
</div>
`
}
test('layout default slot', () => {
const wrapper = mount(Layout, {
slots: {
default: 'Main Content',
footer: '<div>Footer</div>',
header: [
'<div id="one">One</div>',
'<div id="two">Two</div>'
]
}
})
expect(wrapper.html()).toContain('Main Content')
expect(wrapper.find('main').text()).toContain('Main Content') // 更准确
expect(wrapper.html()).toContain('<div>Footer</div>')
expect(wrapper.find('#one').exists()).toBe(true)
expect(wrapper.find('#two').exists()).toBe(true)
})
// 高级使用
import { h } from 'vue'
import Header from './Header.vue'
test('layout full page layout', () => {
const wrapper = mount(Layout, {
slots: {
header: Header
main: h('div', 'Main Content'),
sidebar: { template: '<div>Sidebar</div>' },
footer: '<div>Footer</div>',
}
})
expect(wrapper.html()).toContain('<div>Header</div>')
expect(wrapper.html()).toContain('<div>Main Content</div>')
expect(wrapper.html()).toContain('<div>Footer</div>')
})
// Scoped Slots
const ComponentWithSlots = {
template: `
<div class="scoped">
<slot name="scoped" v-bind="{ msg }" />
</div>
`,
data() {
return {
msg: 'world'
}
}
}
test('scoped slots', () => {
const wrapper = mount(ComponentWithSlots, {
slots: {
scoped: `<template #scoped="params">
Hello {{ params.msg }}
</template>
`
}
})
expect(wrapper.html()).toContain('Hello world')
})
HTTP请求
<template>
<button @click="getPosts">Get posts</button>
<ul>
<li v-for="post in posts" :key="post.id" data-test="post">
{{ post.title }}
</li>
</ul>
</template>
<script>
import axios from 'axios'
export default {
data() {
return {
posts: null
}
},
methods: {
async getPosts() {
this.posts = await axios.get('/api/posts')
}
}
}
</script>
import { mount, flushPromises } from '@vue/test-utils'
import axios from 'axios'
import PostList from './PostList.vue'
const mockPostList = [
{ id: 1, title: 'title1' },
{ id: 2, title: 'title2' }
]
// 下面几行告诉Jest 模拟对 axios.get 的任何调用, 并返回 mockPostList
jest.spyOn(axios, 'get').mockResolvedValue(mockPostList)
test('loads posts on button click', async () => {
const wrapper = mount(PostList)
await wrapper.get('button').trigger('click')
// 断言 axios.get 正确的调用次数
expect(axios.get).toHaveBeenCalledTimes(1)
// 断言 axios.get 正确的调用参数
expect(axios.get).toHaveBeenCalledWith('/api/posts')
// 等待 DOM 更新
await flushPromises()
// 确定更新后的内容
const posts = wrapper.findAll('[data-test="post"]')
expect(posts).toHaveLength(2)
expect(posts[0].text()).toContain('title1')
expect(posts[1].text()).toContain('title2')
})
断言 loading
<template>
<button :disabled="loading" @click="getPosts">Get posts</button>
<p v-if="loading" role="alert">Loading your posts…</p>
<ul v-else>
<li v-for="post in posts" :key="post.id" data-test="post">
{{ post.title }}
</li>
</ul>
</template>
<script>
import axios from 'axios'
export default {
data() {
return {
posts: null,
loading: null
}
},
methods: {
async getPosts() {
this.loading = true
this.posts = await axios.get('/api/posts')
this.loading = null
}
}
}
</script>
test('displays loading state on button click', async () => {
const wrapper = mount(PostList)
// 先判断loading不存在, button 没有 disabled 属性
expect(wrapper.find('[role="alert"]').exists()).toBe(false)
expect(wrapper.get('button').attributes()).not.toHaveProperty('disabled')
await wrapper.get('button').trigger('click')
expect(wrapper.find('[role="alert"]').exists()).toBe(true)
expect(wrapper.get('button').attributes()).toHaveProperty('disabled')
// 等待 DOM 更行
await flushPromises()
// http 请求完成后, 判断loaing button 状态是否重置
expect(wrapper.find('[role="alert"]').exists()).toBe(false)
expect(wrapper.get('button').attributes()).not.toHaveProperty('disabled')
})