赞
踩
Vue 默认安装版本已更新为 v3,本文使用 v2 学习 Vue 应用测试。
Vue 官方推荐了两个用于组件测试的框架:
下面学习使用 Vue Test Utils
官方介绍了手动在应用中集成测试工具的过程。
我们也可以使用 Vue CLI 创建自带测试工具的 Vue 应用,省略这些繁琐安装:
# 使用 Vue CLI 创建项目
npx @vue/cli create vue-testing-demo
? Please pick a preset: Manually select features
# 勾选单元测试 Unit
? Check the features needed for your project: Babel, Router, Vuex, Linter, Unit
? Choose a version of Vue.js that you want to start the project with 2.x
? Use history mode for router? (Requires proper server setup for index fallback in production) No
? Pick a linter / formatter config: Standard
? Pick additional lint features: Lint on save, Lint and fix on commit
# 测试方案选择 Jest
? Pick a unit testing solution: Jest
? Where do you prefer placing config for Babel, ESLint, etc.? In dedicated config files
? Save this as a preset for future projects? No
安装完成后,可以看到项目根目录下有个 tests
目录,里面有个存放单元测试的目录 unit
,里面存放了一个单元测试文件 example.spec.js
。
内容是声明了一个 describe
块,导入了 HelloWorld
组件,通过 it()
创建了一个测试用例,测试组件渲染后的文本内容是否和期望的一样。
import { shallowMount } from '@vue/test-utils'
import HelloWorld from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
it('renders props.msg when passed', () => {
const msg = 'new message'
const wrapper = shallowMount(HelloWorld, {
propsData: { msg }
})
expect(wrapper.text()).toMatch(msg)
})
})
npm run test:unit
运行测试脚本,可以看到测试结果。
Vue CLI 使用一个预设配置 Jest:
// jest.config.js
module.exports = {
preset: '@vue/cli-plugin-unit-jest'
}
实际配置来源于 node_modules\@vue\cli-plugin-unit-jest\presets\default\jest-preset.js
:
// node_modules\@vue\cli-plugin-unit-jest\presets\default\jest-preset.js // ... module.exports = { testEnvironment: 'jsdom', // 指定 Jest 测试中加载模块时可以省略的后缀名 moduleFileExtensions: [ 'js', 'jsx', 'json', // tell Jest to handle *.vue files 'vue' ], // 文件转换(Jest 默认只处理 js 文件) transform: { // process *.vue files with vue-jest '^.+\\.vue$': vueJest, '.+\\.(css|styl|less|sass|scss|jpg|jpeg|png|svg|gif|eot|otf|webp|ttf|woff|woff2|mp4|webm|wav|mp3|m4a|aac|oga|avif)$': require.resolve('jest-transform-stub'), '^.+\\.jsx?$': require.resolve('babel-jest') }, // 转换忽略的文件 transformIgnorePatterns: ['/node_modules/'], // support the same @ -> src alias mapping in source code // 模块名称映射 moduleNameMapper: { '^@/(.*)$': '<rootDir>/src/$1' }, // serializer for snapshots // 快照的序列化器 // vue 组件渲染的结果保存到快照前的序列化操作 snapshotSerializers: [ 'jest-serializer-vue' ], // 测试文件匹配规则 testMatch: [ '**/tests/unit/**/*.spec.[jt]s?(x)', '**/__tests__/*.[jt]s?(x)' ], // https://github.com/facebook/jest/issues/6766 // 测试时提供给 jsdom 的基准环境路径(涉及 URL 相关测试) testURL: 'http://localhost/', // 监听相关的插件(提供了一些命令行的辅助工具) watchPlugins: [ require.resolve('jest-watch-typeahead/filename'), require.resolve('jest-watch-typeahead/testname') ] }
官方文档:教程 | Vue Test Utils
Vue Test Utils 提供的 mount
和 shallowMount
(不渲染子组件)方法用来快速渲染挂载组件,省略了创建 DOM 节点,调用 render 渲染组件的过程。
挂载组件返回一个包裹器,包含很多封装、遍历和查询其内部 Vue 组件实例的便捷方法。
<!-- src\components\HelloWorld.vue --> <template> <div class="hello"> <button @click="count += 1">Click</button> <p data-testid="count-text">{{ count }}</p> <h1>{{ msg }}</h1> </div> </template> <script> export default { name: 'HelloWorld', props: { msg: String }, data () { return { count: 0 } } } </script>
// tests\unit\example.spec.js import { shallowMount } from '@vue/test-utils' import HelloWorld from '@/components/HelloWorld.vue' test('HelloWorld.vue', () => { // 挂载组件,获得一个包裹器 const wrapper = shallowMount(HelloWorld, { // 模拟 props propsData: { msg: 'Hello World' } }) // 组件实例 console.log(wrapper.vm) // wrapper.element - 组件根节点 console.log(wrapper.element.outerHTML) // wrapper 包含很多辅助方法,上面打印内容也可以写作: console.log(wrapper.html()) // 检查是否包含指定字符串 expect(wrapper.html()).toContain('Hello World') })
test('点击按钮,count 为 1', () => {
const wrapper = shallowMount(HelloWorld)
// 注意:find 已废弃,将在未来版本删除,使用 findComponent 替换
// const button = wrapper.find('button')
const button = wrapper.findComponent('button')
const countText = wrapper.findComponent('[data-testid="count-text"]')
// 触发事件
button.trigger('click')
expect(wrapper.vm.count).toBe(1) // 测试成功
expect(countText.text()).toBe('1') // 测试失败:实际内容是 '0'
})
上面交互测试虽然 js 中的 wrapper.vm.count
已经更改,但是 DOM 中的内容并没有更新,这是因为 Vue 会对未生效的 DOM 进行批量异步更新,避免因数据反复变化而导致不必要的渲染。
所以任何导致操作 DOM 的改变,都应该在更新响应式属性之后,断言之前等待 Vue 完成 DOM 更新。
我们在 Vue 项目中经常使用 $nextTick
实例方法等待 DOM 更新完成,测试代码中也可以使用:
// 引入 Vue import Vue from 'vue' test('点击按钮,文本内容为 1', done => { const wrapper = shallowMount(HelloWorld) const button = wrapper.findComponent('button') const countText = wrapper.findComponent('[data-testid="count-text"]') // 触发事件 button.trigger('click') wrapper.vm.$nextTick(() => { expect(countText.text()).toBe('1') // 测试成功 done() }) // 或者,也可以使用全局方法 Vue.nextTick,它返回一个 Promise // 注意:使用 await 要给测试函数添加 async 关键字 // 注意:在 Vue.nextTick 回调中执行断言有一些问题,稍后会介绍 // await Vue.nextTick() })
trigger
方法也会返回一个 Promise,我们可以 await trigger
,然后再执行断言(注意测试函数要添加 async
关键字):
test('点击按钮,文本内容为 1', async () => {
const wrapper = shallowMount(HelloWorld)
const button = wrapper.findComponent('button')
const countText = wrapper.findComponent('[data-testid="count-text"]')
// 等待 Vue 完成 DOM 更新
await button.trigger('click')
expect(countText.text()).toBe('1') // 测试成功
})
如果不喜欢使用 async/await
也可以在 trigger().then()
回调中执行断言(不会像 nextTick
一样无法捕获,稍后会讲) :
test('点击按钮,count 为 1', () => {
const wrapper = shallowMount(HelloWorld)
const button = wrapper.findComponent('button')
const countText = wrapper.findComponent('[data-testid="count-text"]')
// 等待 Vue 完成 DOM 更新
button.trigger('click').then(() => {
expect(countText.text()).toBe('1')
})
})
nextTick
当你在测试代码中使用 Vue.nextTick
,并在回调中使用断言,断言抛出的错误可能不会被测试运行器捕获(尽管使用了 done
),因为内部使用了 Promise:
test('错误不会被捕获,该测试将超时', done => {
Vue.nextTick(() => {
expect(true).toBe(false)
done()
})
})
关于这个问题,官方有两个建议:
test('建议1:修改 Vue 全局错误处理器,设置为 `done` 回调', done => { Vue.config.errorHandler = done Vue.nextTick(() => { expect(true).toBe(false) // 注意:这里仍要调用 done,否则断言成功后,测试会继续等待直到超时 done() }) }) test('建议2:在调用 `nextTick` 时不带参数,让其作为一个 Promise 返回', () => { return Vue.nextTick().then(() => { expect(true).toBe(false) }) }) test('建议2:使用 async/await 写法', async () => { await Vue.nextTick() expect(true).toBe(false) })
注意:这里讲的是 Vue 全局方法
Vue.nextTick
,实例方法vm.$nextTick()
可以在回调中执行断言,等待done
。
每个挂载的包裹器都会通过其背后的 Vue 实例自动记录所有被触发的事件,可以使用 wrapper.emitted()
方法取回这些事件记录。
<!-- src\components\EventEmit.vue -->
<template>
<div>
<button data-testid="btn1" @click="$emit('foo')">按钮1</button>
<button data-testid="btn2" @click="$emit('foo', 123)">按钮2</button>
<button data-testid="btn3" @click="$emit('bar')">按钮3</button>
</div>
</template>
<script>
export default {}
</script>
import { shallowMount } from '@vue/test-utils' import EventEmit from '@/components/EventEmit.vue' test('断言触发的事件', () => { const wrapper = shallowMount(EventEmit) const btn1 = wrapper.findComponent('[data-testid="btn1"]') const btn2 = wrapper.findComponent('[data-testid="btn2"]') const btn3 = wrapper.findComponent('[data-testid="btn3"]') // 通过点击按钮触发事件 btn1.trigger('click') btn2.trigger('click') btn3.trigger('click') // 通过实例触发事件 wrapper.vm.$emit('foo', 'from vm.$emit') // 获取事件记录 console.log(wrapper.emitted()) // 打印结果: // { // foo: [ [], [ 123 ], [ 'from vm.$emit' ] ], // bar: [ [] ] // } // 断言事件已经被触发 expect(wrapper.emitted().foo).toBeTruthy() // 断言事件的数量 expect(wrapper.emitted().foo.length).toBe(3) // 断言事件的有效数据 expect(wrapper.emitted().foo[1]).toEqual([123]) // 获取一个按触发先后排序的事件数组 console.log(wrapper.emittedByOrder()) // 注意:emittedByOrder 已废弃,将在未来版本移除,当前使用会抛出error提示(不影响测试结果) // 目前没有其他可获取顺序的替代API // 开发者认为断言事件的顺序是脆弱的不那么关键的测试。 // ISSUE - https://github.com/vuejs/vue-test-utils/issues/1775 // 打印结果: // [ // { name: 'foo', args: [] }, // { name: 'foo', args: [ 123 ] }, // { name: 'bar', args: [] }, // { name: 'foo', args: [ 'from vm.$emit' ] } // ] })
Vue Test Utils 提供的 mount
和 shallowMount
方法用来快速渲染挂载组件。
<!-- src\components\Foo.vue -->
<template>
<div>
<Bar />
</div>
</template>
<script>
import Bar from './Bar'
export default {
components: { Bar }
}
</script>
<!-- src\components\Bar.vue -->
<template>
<div>Bar 组件</div>
</template>
// tests\unit\example.spec.js import { shallowMount, mount } from '@vue/test-utils' import Foo from '@/components/Foo.vue' test.only('Mount Test', () => { const shallowMountWrapper = shallowMount(Foo) const mountWrapper = mount(Foo) // shallowMountWrapper 是浅渲染,不会渲染子组件,使用 stub 标记占位(存根) console.log(shallowMountWrapper.html()) // <div> // <bar-stub></bar-stub> // </div> // mountWrapper 是深渲染,完全渲染所有子组件 console.log(mountWrapper.html()) // <div> // <div>Bar 组件</div> // </div> })
如果测试的组件不关心子组件,建议使用 shallowMount
降低资源消耗。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。