做单元测试的优点:
1.减少bug避免低级错误
2.提高代码运行质量
3.快速定位问题
4.减少调试时间,提高开发效率
5.便于重构
Jest安装:
npm install babel-jest jest jest-serializer-vue @vue/test-utils @vue/cli-plugin-unit-jest -D
配置
vueCli内置了一套jest配置预置文件,一般情况下直接引用即可,如有特殊配置可见下文配置释意。
// jest.config.js
module.exports = {
preset: '@vue/cli-plugin-unit-jest'
}
配置项目释意
module.exports = {
// 预设,项目中一版可直接使用vue/cli预设的库就行
preset: '@vue/cli-plugin-unit-jest',
// 多用于一个测试文件运行时展示每个测试用例测试通过情况
verbose: true,
// 参数指定只要有一个测试用例没有通过,就停止执行后面的测试用例
bail: true,
// 测试环境,jsdom 可以在 Node 虚拟浏览器环境运行测试
testEnvironment: 'jsdom',
// 需要检测的文件类型(不需要配置)
moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
// 预处理器配置,匹配的文件要经过转译才能被识别,否则会报错(不需要配置)
transform: {
// 用 `vue-jest` 处理 `*.vue` 文件
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest",
// 用 `babel-jest` 处理 js
"^.+\\.js$": "babel-jest"
},
// 转译时忽略 node_modules
transformIgnorePatterns: ['/node_modules/'],
// 从正则表达式到模块名称的映射,和webpack的alias类似(不需要配置)
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
// Jest用于检测测试的文件,可以用正则去匹配
testMatch: [
'**/tests/unit/**/*.spec.[jt]s?(x)',
'**/__tests__/*.[jt]s?(x)'
],
// 是否显示覆盖率报告,开启后显示代码覆盖率详细信息,将测试用例结果输出到终端
collectCoverage: true,
// 告诉 jest 哪些文件需要经过单元测试测试
collectCoverageFrom: ["src/**/*.{js,vue}", "!**/node_modules/**"],
// 覆盖率报告输出的目录
coverageDirectory: 'tests/unit/coverage',
// 报告的格式
coverageReporters: ["html", "text-summary"],
// 需要跳过覆盖率信息收集的文件目录
coveragePathIgnorePatterns: ['/node_modules/'],
// 设置单元测试覆盖率阈值, 如果未达到阈值,Jest 将返回失败
coverageThreshold: {
global: {
statements: 60, // 保证每个语句都执行了
functions: 60, // 保证每个函数都调用了
branches: 60, // 保证每个 if 等分支代码都执行了
lines: 60
},
},
// Jest在快照测试中使用的快照序列化程序模块的路径列表
snapshotSerializers: ["<rootDir>/node_modules/jest-serializer-vue"]
}
相关API:
test(name, fn, timeout)
test 有个别名 it,两个方法是一样的。
name:描述测试用例名称。
fn:期望测试的函数,也是测试用例的核心。
timeout(可选):超时时间,也就是超过多久将会取消测试(默认是5秒钟)
断言类api
toBeNull:只匹配 null ;
toBeNaN:只匹配 NaN ;
toBeUndefined:只匹配 undefined ;
toBeDefined:与 toBeUndefined 相反 ;
toBeTruthy:匹配任何 if 语句为真 ;
toBeFalsy:匹配任何 if 语句为假 ;
toBeGreaterThan :匹配数字时使用,期望大于,即 result > x ;
toBeGreaterThanOrEqual :匹配数字时使用,期望大于等于,即 result > = x ;
toBeLessThan :匹配数字时使用,期望小于,即 result < x ;
toBeLessThanOrEqual :匹配数字时使用,期望小于等于,即 result <= x ;
toBeCloseTo:小数点精度问题匹配,例如 0.1+0.2 != 0.3,但我们期望它等于,就需要使用toBeCloseTo
toEqual 对象、数组的深度匹配。递归检查对象或数组的每个字段。
和toBe的区别是,toBe 匹配对象对比的是内存地址,toEqual 对比的是属性值。即toBe是===,toEqual是==
not 不匹配
toMatch 匹配字符串时使用,期望字符串包含另一个字符串。
toContain 检查一个数组中是否包含一个值时使用。
toBeCalled 函数被调用了
toThrow()----支持字符串,浮点数,变量
toMatchSnapshot()----jest特有的快照测试
编写用例:
Jest的单元测试核心就是在 test 方法的第二个参数里面,expect 方法返回一个期望对象,
通过匹配器(例如toBe)进行断言,期望是否和你预期一致,和预期一致则单元测试通过,不一致则测试无法通过,需要排除问题然后继续进行单元测试。
// HelloWorld.vue
<template>
<div>
<h3>{{ contextNum }}</h3>
<button class="btn" @click="increment">+</button>
</div>
</template>
<script>
export default {
name: 'HelloWorld',
data() {
return {
contextNum: 0
}
},
methods: {
increment() {
this.contextNum ++
}
}
}
</script>
// @/tests/unit/HelloWorld.spec.js
import { mount } from "@vue/test-utils";
import Counter from '@/components/HelloWorld.vue'
describe('HelloWorld.vue', () => {
const wrapper = mount(Counter)
// 渲染
it('renders', () => {
expect(wrapper.html()).toContain('<h3>0</h3>')
})
// 是否有按钮
it('has a button', () => {
expect(wrapper.find('button').exists()).toBeTruthy()
})
// 模拟用户交互
// 使用 nextTick 与 await
it('button click', async () => {
expect(wrapper.vm.count).toBe(0)
const button = wrapper.find('button')
await button.trigger('click')
expect(wrapper.vm.count).toBe(1)
})
}
测试报告:
–coverage 生成测试覆盖率
–watch 单文件监视测试
–watchAll 监视所有文件改动,测试相应的测试。
可以通过修改 package.json 命令行来生成
“test:unit”: “vue-cli-service test:unit --coverage”
效果:
具体参数含义:
Statements: 语句覆盖率,执行到每个语句;
Branches: 分支覆盖率,执行到每个if代码块;
Functions: 函数覆盖率,调用到程式中的每一个函数;
Lines: 行覆盖率, 执行到程序中的每一行
持续监听:
为了提高效率,可以通过加启动参数的方式让 jest 持续监听文件的修改,而不需要每次修改完再重新执行测试用例。修改 package.json
"test:unit": "vue-cli-service test:unit --watchAll",
异步测试:
1.done
将 it 函数的第二个参数由无参回调改为一个接收一个 done 参数的回调,Jest 会等 done 回调函数执行结束后,结束测试。
describe('fetchData', () => {
it('done异步函数测试返回值是否一致', (done) => {
const callback = data => {
try {
expect(data).toBe('hello')
done()
} catch (error) {
done(error) // 用于捕获错误的原因否则控制台报错超时
}
}
fetchData(callback)
})
})
2.async await
test('success', async () => {
const data = await fetchData(true)
expect(data).toBe('success message')
})
test('error', async () => {
expect.assertions(1)
try {
await fetchData(false)
} catch (error) {
expect(error).toBe('error message')
}
})
钩子函数:(执行顺序为1->2->3->4)
①、beforeAll(fn, timeout)
文件内所有测试开始前执行的钩子函数。
使用 beforeAll 设置一些在测试用例之间共享的全局状态。
②、afterAll(fn, timeout)
文件内所有测试完成后执行的钩子函数。
使用 afterAll 清理一些在测试用例之间共享的全局状态。
③、beforeEach(fn, timeout)
文件内所有测试开始前执行的钩子函数。
使用 beforeAll 设置一些在测试用例之间共享的全局状态。
④、afterEach(fn, timeout)
文件内每个测试完成后执行的钩子函数。
使用 afterEach 清理一些在每个测试中创建的临时状态。
全局插件:
如果需要安装所有 test 都使用到的全局插件,例如antdDesignVue,可以使用 setupFiles,首先需要在 jest.config.js 文件中指定 setup 文件。
// jest.config.js
module.exports = {
setupFiles: ['<rootDir>/tests/unit/specs/setup.js']
}
然后在 tests/unit/specs 目录下创建 setup.js 文件
import Vue from 'vue'
// 以下全局注册的插件在jest中不生效,必须使用localVue
import antDesignVue from 'ant-design-vue'
Vue.use(antDesignVue)
// 阻止启动生产消息,常用作指令。
Vue.config.productionTip = false
单独测试文件使用某些插件时,可以使用 localVue 来创建一个临时的 Vue 实例。
import { createLocalVue, mount } from '@vue/test-utils'
import antDesignVue from 'ant-design-vue'
// 引入组件
import HelloWorld from '@/components/HelloWorld.vue'
// createLocalVue 返回一个 Vue 的类供你添加组件、混入和安装插件而不会污染全局的 Vue 类。
const localVue = createLocalVue()
localVue.use(antDesignVue)
// describe 代表一个作用域
describe('HelloWorld.vue', () => {
// 创建一个包含被挂载和渲染的 Vue 组件的 Wrapper
// 在挂载选项中传入 localVue
const wrapper = mount(HelloWorld, {
localVue,
propsData: {
}
})
// input create 这里是一个自定义的描述性文字
it('input create', async ()=> {
expect(wrapper.find('input').exists()).toBeTruthy()
// classes() 方法,返回 class 名称的数组。或在提供 class 名的时候返回一个布尔值
// toBe 和toEqual 类似,区别在于toBe 更严格限于同一个对象,如果是基本类型则没什么区别
expect(wrapper.classes('el-input')).toBe(true)
})
}
Vue Test Utils相关:
它模拟了一部分类似 jQuery 的 API,非常直观并且易于使用和学习,提供了一些接口和几个方法来减少测试的样板代码,方便判断、操纵和遍历 Vue Component 的输出,并且减少了测试代码和实现代码之间的耦合。
一般使用其 mount() 或 shallowMount() 方法,将目标组件转化为一个 Wrapper 对象,并在测试中调用其各种方法,例如:
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
describe('Foo', () => {
it('renders a div', () => {
const wrapper = mount(Foo)
expect(wrapper.contains('div')).toBe(true)
})
})
固定在 DOM 上:
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
describe('Foo', () => {
it('renders a div', () => {
const div = document.createElement('div')
document.body.appendChild(div)
const wrapper = mount(Foo, {
attachTo: div
})
expect(wrapper.contains('div')).toBe(true)
wrapper.destroy()
})
})
默认插槽和具名插槽:
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
import FooBar from './FooBar.vue'
describe('Foo', () => {
it('renders a div', () => {
const wrapper = mount(Foo, {
slots: {
default: [Bar, FooBar],
fooBar: FooBar, // 将匹配 `<slot name="FooBar" />`。
foo: '<div />'
}
})
expect(wrapper.contains('div')).toBe(true)
})
})
将全局属性存根:
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
describe('Foo', () => {
it('renders a div', () => {
const $route = { path: 'http://www.example-path.com' }
const wrapper = mount(Foo, {
mocks: {
$route
}
})
expect(wrapper.vm.$route.path).toBe($route.path)
})
})
将组件存根:
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
import Faz from './Faz.vue'
describe('Foo', () => {
it('renders a div', () => {
const wrapper = mount(Foo, {
stubs: {
BarFoo: true,
FooBar: Faz,
Bar: { template: '<div class="stubbed" />' }
}
})
expect(wrapper.contains('.stubbed')).toBe(true)
expect(wrapper.contains(Bar)).toBe(true)
})
})
render()-服务端api。将一个对象渲染成为一个字符串并返回一个 cheerio 包裹器。
import { render } from '@vue/server-test-utils'
import Foo from './Foo.vue'
describe('Foo', () => {
it('renders a div', async () => {
const wrapper = await render(Foo)
expect(wrapper.html()).toContain('<div></div>')
})
})
import { render } from '@vue/server-test-utils'
import Foo from './Foo.vue'
describe('Foo', () => {
it('renders a div', async () => {
const wrapper = await render(Foo, {
propsData: {
color: 'red'
}
})
expect(wrapper.html()).toContain('red')
})
})
renderToString()-
renderToString 在底层使用 vue-server-renderer 将一个组件渲染为 HTML。
import { renderToString } from '@vue/server-test-utils'
import Foo from './Foo.vue'
describe('Foo', () => {
it('renders a div', async () => {
const str = await renderToString(Foo)
expect(str).toContain('<div></div>')
})
})
import { renderToString } from '@vue/server-test-utils'
import Foo from './Foo.vue'
describe('Foo', () => {
it('renders a div', async () => {
const str = await renderToString(Foo, {
propsData: {
color: 'red'
}
})
expect(str).toContain('red')
})
})
import { renderToString } from '@vue/server-test-utils'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
import FooBar from './FooBar.vue'
describe('Foo', () => {
it('renders a div', async () => {
const str = await renderToString(Foo, {
slots: {
default: [Bar, FooBar],
fooBar: FooBar, // Will match <slot name="FooBar" />,
foo: '<div />'
}
})
expect(str).toContain('<div></div>')
})
})
Ref
Vue Test Utils 允许通过一个查找选项对象在组件包裹器上根据 $ref 选择元素。
const buttonWrapper = wrapper.find({ ref: 'myButton' })
buttonWrapper.trigger('click')
``
createLocalVue()-createLocalVue 返回一个 Vue 的类供你添加组件、混入和安装插件而不会污染全局的 Vue 类。
在组件渲染功能和观察者期间,errorHandler选项可用于处理未捕获的错误。
```java
import { createLocalVue, shallowMount } from '@vue/test-utils'
import MyPlugin from 'my-plugin'
import Foo from './Foo.vue'
const localVue = createLocalVue()
localVue.use(MyPlugin)
const wrapper = shallowMount(Foo, {
localVue,
mocks: { foo: true }
})
expect(wrapper.vm.foo).toBe(true)
const freshWrapper = shallowMount(Foo)
expect(freshWrapper.vm.foo).toBe(false)
import { createLocalVue, shallowMount } from '@vue/test-utils'
import Foo from './Foo.vue'
const errorHandler = (err, vm, info) => {
expect(err).toBeInstanceOf(Error)
}
const localVue = createLocalVue({
errorHandler
})
// Foo在生命周期挂钩中引发错误
const wrapper = shallowMount(Foo, {
localVue
})
其中Warpper是一个对象,该对象包含了一个挂载的组件或 vnode,以及测试该组件或 vnode 的方法。
warper的属性:
属性
vm-这是该 Vue 实例。你可以通过 wrapper.vm 访问一个实例所有的方法和属性。这只存在于 Vue 组件包裹器或绑定了 Vue 组件包裹器的 HTMLElement 中
element-包裹器的根 DOM 节点
selector-被 find() 或 findAll() 创建这个 wrapper 时使用的选择器
方法
attributes-返回 Wrapper DOM 节点的特性对象。如果提供了 key,则返回这个 key 对应的值
```javascript
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
const wrapper = mount(Foo)
expect(wrapper.attributes().id).toBe('foo')
expect(wrapper.attributes('id')).toBe('foo')
classes-返回 class 名称的数组。或在提供 class 名的时候返回一个布尔值。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
const wrapper = mount(Foo)
expect(wrapper.classes()).toContain('bar')
expect(wrapper.classes('bar')).toBe(true)
contains-判断 Wrapper 是否包含了一个匹配选择器的元素或组件。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
const wrapper = mount(Foo)
expect(wrapper.contains('p')).toBe(true)
expect(wrapper.contains(Bar)).toBe(true)
destroy-销毁一个 Vue 组件实例。
import { mount } from '@vue/test-utils'
import sinon from 'sinon'
const spy = sinon.stub()
mount({
render: null,
destroyed() {
spy()
}
}).destroy()
expect(spy.calledOnce).toBe(true)
emitted-返回一个包含由 Wrapper vm 触发的自定义事件的对象
import { mount } from '@vue/test-utils'
test('emit demo', async () => {
const wrapper = mount(Component)
wrapper.vm.$emit('foo')
wrapper.vm.$emit('foo', 123)
await wrapper.vm.$nextTick() // 等待事件处理完成
/*
wrapper.emitted() 返回如下对象:
{
foo: [[], [123]]
}
*/
// 断言事件已经被触发
expect(wrapper.emitted().foo).toBeTruthy()
// 断言事件的数量
expect(wrapper.emitted().foo.length).toBe(2)
// 断言事件的有效数据
expect(wrapper.emitted().foo[1]).toEqual([123])
})
emittedByOrder-返回一个包含由 Wrapper vm 触发的自定义事件的数组。
import { mount } from '@vue/test-utils'
const wrapper = mount(Component)
wrapper.vm.$emit('foo')
wrapper.vm.$emit('bar', 123)
/*
`wrapper.emittedByOrder() 返回如下数组:
[
{ name: 'foo', args: [] },
{ name: 'bar', args: [123] }
]
*/
// 断言事件的触发顺序
expect(wrapper.emittedByOrder().map(e => e.name)).toEqual(['foo', 'bar'])
exists-如果被一个空 Wrapper 或 WrapperArray 调用则返回 false。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
const wrapper = mount(Foo)
expect(wrapper.exists()).toBe(true)
expect(wrapper.find('does-not-exist').exists()).toBe(false)
expect(wrapper.findAll('div').exists()).toBe(true)
expect(wrapper.findAll('does-not-exist').exists()).toBe(false)
find-返回匹配选择器的第一个 DOM 节点或 Vue 组件的 Wrapper。
可以使用任何有效的 DOM 选择器 (使用 querySelector 语法)。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
const wrapper = mount(Foo)
const div = wrapper.find('div')
expect(div.exists()).toBe(true)
const byId = wrapper.find('#bar')
expect(byId.element.id).toBe('bar')
findAll-可以使用任何有效的选择器。返回一个 WrapperArray。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
const wrapper = mount(Foo)
const div = wrapper.findAll('div').at(0)
expect(div.is('div')).toBe(true)
const bar = wrapper.findAll(Bar).at(0) // 已废弃的用法
expect(bar.is(Bar)).toBe(true)
findComponent-返回第一个匹配的 Vue 组件的 Wrapper。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
import Bar from './Bar.vue'
const wrapper = mount(Foo)
const bar = wrapper.findComponent(Bar) // => 通过组件实例找到 Bar
expect(bar.exists()).toBe(true)
const barByName = wrapper.findComponent({ name: 'bar' }) // => 通过 `name` 找到 Bar
expect(barByName.exists()).toBe(true)
const barRef = wrapper.findComponent({ ref: 'bar' }) // => 通过 `ref` 找到 Bar
expect(barRef.exists()).toBe(true)
findAllComponents-为所有匹配的 Vue 组件返回一个 WrapperArray。
wraper.html()-返回 Wrapper DOM 节点的 HTML 字符串。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
const wrapper = mount(Foo)
expect(wrapper.html()).toBe('<div><p>Foo</p></div>')、
get-和 find 工作起来一样,但是如果未匹配到给定的选择器时会抛出错误。当搜索一个可能不存在的元素时你应该使用 find。当获取一个应该存在的元素时你应该使用这个方法,并且如果没有找到的话它会提供一则友好的错误信息。
import { mount } from '@vue/test-utils'
const wrapper = mount(Foo)
// 和 `wrapper.find` 相似。
// 如果 `get` 没有找到任何元素将会抛出一个而错误。`find` 则不会做任何事。
expect(wrapper.get('.does-exist'))
expect(() => wrapper.get('.does-not-exist'))
.to.throw()
.with.property(
'message',
'Unable to find .does-not-exist within: <div>the actual DOM here...</div>'
)
isEmpty-断言 Wrapper 并不包含子节点。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
const wrapper = mount(Foo)
expect(wrapper.isEmpty()).toBe(true)
isVisible-断言 Wrapper 是否可见。
如果有一个祖先元素拥有 display: none 或 visibility: hidden 样式则返回 false。
这可以用于断言一个组件是否被 v-show 所隐藏。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
const wrapper = mount(Foo)
expect(wrapper.isVisible()).toBe(true)
expect(wrapper.find('.is-not-visible').isVisible()).toBe(false)
#props-返回 Wrapper vm 的 props 对象。如果提供了 key,则返回这个 key 对应的值。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
const wrapper = mount(Foo, {
propsData: {
bar: 'baz'
}
})
expect(wrapper.props().bar).toBe('baz')
expect(wrapper.props('bar')).toBe('baz')
setChecked-设置 checkbox 或 radio 类 元素的 checked 值并更新 v-model 绑定的数据
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
test('setChecked demo', async () => {
const wrapper = mount(Foo)
const radioInput = wrapper.find('input[type="radio"]')
await radioInput.setChecked()
expect(radioInput.element.checked).toBeTruthy()
})
setData-setData 通过递归调用 Vue.set 生效。
import { mount } from '@vue/test-utils'
import Foo from './Foo.vue'
test('setData demo', async () => {
const wrapper = mount(Foo)
await wrapper.setData({ foo: 'bar' })
expect(wrapper.vm.foo).toBe('bar')
})
#trigger-在该 Wrapper DOM 节点上异步触发一个事件。
import { mount } from '@vue/test-utils'
import sinon from 'sinon'
import Foo from './Foo'
test('trigger demo', async () => {
const clickHandler = sinon.stub()
const wrapper = mount(Foo, {
propsData: { clickHandler }
})
await wrapper.trigger('click')
await wrapper.trigger('click', {
button: 0
})
await wrapper.trigger('click', {
ctrlKey: true // 用于测试 @click.ctrl 处理函数
})
expect(clickHandler.called).toBe(true)
})
context-将上下文传递给函数式组件。该选项只能用于函数式组件。
import Foo from './Foo.vue'
import Bar from './Bar.vue'
const wrapper = mount(Component, {
context: {
props: { show: true },
children: [Foo, Bar]
}
})
expect(wrapper.is(Component)).toBe(true)
data-向一个组件传入数据。这将会合并到现有的 data 函数中。
const Component = {
template: `
<div>
<span id="foo">{{ foo }}</span>
<span id="bar">{{ bar }}</span>
</div>
`,
data() {
return {
foo: 'foo',
bar: 'bar'
}
}
}
const wrapper = mount(Component, {
data() {
return {
bar: 'my-override'
}
}
})
wrapper.find('#foo').text() // 'foo'
wrapper.find('#bar').text() // 'my-override'
slots-为组件提供一个 slot 内容的对象。该对象中的键名就是相应的 slot 名,键值可以是一个组件、一个组件数组、一个字符串模板或文本。
import Foo from './Foo.vue'
import MyComponent from './MyComponent.vue'
const bazComponent = {
name: 'baz-component',
template: '<p>baz</p>'
}
const yourComponent = {
props: {
foo: {
type: String,
required: true
}
},
render(h) {
return h('p', this.foo)
}
}
const wrapper = shallowMount(Component, {
slots: {
default: [Foo, '<my-component />', 'text'],
fooBar: Foo, // 将会匹配 `<slot name="FooBar" />`.
foo: '<div />',
bar: 'bar',
baz: bazComponent,
qux: '<my-component />',
quux: '<your-component foo="lorem"/><your-component :foo="yourProperty"/>'
},
stubs: {
// 用来注册自定义组件
'my-component': MyComponent,
'your-component': yourComponent
},
mocks: {
// 用来向渲染上下文添加 property
yourProperty: 'ipsum'
}
})
expect(wrapper.find('div')).toBe(true)
scopedSlots-提供一个该组件所有作用域插槽的对象。每个键对应到插槽的名字。
shallowMount(Component, {
scopedSlots: {
foo: '<p slot-scope="foo">{{foo.index}},{{foo.text}}</p>'
}
})
stubs-将子组件存根。可以是一个要存根的组件名的数组或对象。如果 stubs 是一个数组,则每个存根都是一个 <${component name}-stub>。
import Foo from './Foo.vue'
mount(Component, {
stubs: ['registered-component']
})
shallowMount(Component, {
stubs: {
// 使用一个特定的实现作为存根
'registered-component': Foo,
// 使用创建默认的实现作为存根。
// 这里默认存根的组件名是 `another-component`。
// 默认存根是 `<${the component name of default stub}-stub>`。
'another-component': true
}
})
mocks-为实例添加额外的属性。在伪造全局注入的时候有用。
const $route = { path: 'http://www.example-path.com' }
const wrapper = shallowMount(Component, {
mocks: {
$route
}
})
expect(wrapper.vm.$route.path).toBe($route.path)
attachTo-
指定一个 HTMLElement 或定位到一个 HTML 元素的 CSS 选择器字符串,组件将会被完全挂载到文档中的这个元素。
当要挂载到 DOM 时,你需要在测试的结尾调用 wrapper.destroy() 以将该元素从文档中移除,并销毁该组件实例。
const Component = {
template: '<div>ABC</div>'
}
let wrapper = mount(Component, {
attachTo: '#root'
})
expect(wrapper.vm.$el.parentNode).to.not.be.null
wrapper.destroy()
wrapper = mount(Component, {
attachTo: document.getElementById('root')
})
expect(wrapper.vm.$el.parentNode).to.not.be.null
wrapper.destroy()
propsData-在组件被挂载时设置组件实例的 prop。
const Component = {
template: '<div>{{ msg }}</div>',
props: ['msg']
}
const wrapper = mount(Component, {
propsData: {
msg: 'aBC'
}
})
expect(wrapper.text()).toBe('aBC')
listeners-设置组件实例的 $listeners 对象。
const Component = {
template: '<button v-on:click="$emit(\'click\')"></button>'
}
const onClick = jest.fn()
const wrapper = mount(Component, {
listeners: {
click: onClick
}
})
wrapper.trigger('click')
expect(onClick).toHaveBeenCalled()
parentComponent-用来作为被挂载组件的父级组件。
import Foo from './Foo.vue'
const wrapper = shallowMount(Component, {
parentComponent: Foo
})
expect(wrapper.vm.$parent.$options.name).toBe('foo')
provide-为组件传递用于注入的属性
const Component = {
inject: ['foo'],
template: '<div>{{this.foo()}}</div>'
}
const wrapper = shallowMount(Component, {
provide: {
foo() {
return 'fooValue'
}
}
})
expect(wrapper.text()).toBe('fooValue')
wraper.toMatchSnapshot()第一次运行快照测试时会生成一个快照文件,之后每次执行测试的时候会生成一个快找然后对比最初生成的快照文件,如狗咩有发生变暖则通过测试。
Mock相关api:
Mock函数的作用
在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只需要知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,使用Mock函数是十分有必要。
Mock函数提供的以下三种特性:
捕获函数调用情况
设置函数返回值
改变函数的内部实现
jest.fn()
jest.fn()是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。
// functions.test.js
test('测试jest.fn()调用', () => {
let mockFn = jest.fn();
let result = mockFn(1, 2, 3);
// 断言mockFn的执行后返回undefined
expect(result).toBeUndefined();
// 断言mockFn被调用
expect(mockFn).toBeCalled();
// 断言mockFn被调用了一次
expect(mockFn).toBeCalledTimes(1);
// 断言mockFn传入的参数为1, 2, 3
expect(mockFn).toHaveBeenCalledWith(1, 2, 3);
})
test('测试jest.fn()内部实现', () => {
let mockFn = jest.fn((num1, num2) => {
return num1 * num2;
})
// 断言mockFn执行后返回100
expect(mockFn(10, 10)).toBe(100);
})
```javascript
// api.js
import axios from 'axios';
export default {
async getProductList(callback) {
return axios.get('https://www.zhifu.api').then(res => {
return callback(res.data);
})
}
}
import fetch from '../src/fetch.js'
test('getProductList中的回调函数应该能够被调用', async () => {
expect.assertions(1);
let mockFn = jest.fn();
await fetch.getProductList(mockFn);
// 断言mockFn被调用
expect(mockFn).toBeCalled();
})
jest.spyOn()
jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。实际上,jest.spyOn()是jest.fn()的语法糖,它创建了一个被spy的函数具有相同内部代码的mock函数。
// functions.test.js
import events from '../src/events';
import api from '../src/api';
test('使用jest.spyOn()api.getProductList被正常调用', async() => {
expect.assertions(2);
const spyFn = jest.spyOn(fetch, 'getProductList');
await events.getProductList();
expect(spyFn).toHaveBeenCalled();
expect(spyFn).toHaveBeenCalledTimes(1);
})
jest.mock()
etch.js文件夹中封装的请求方法可能我们在其他模块被调用的时候,并不需要进行实际的请求(请求方法已经通过单侧或需要该方法返回非真实数据)。此时,使用jest.mock()去mock整个模块是十分有必要的。
// events.js
import fetch from './api';
export default {
async getProductList() {
return fetch.getProductList(data => {
console.log('getProductList be called!');
// do something
});
}
}
// functions.test.js
import events from '../src/events';
import fetch from '../src/api';
jest.mock('../src/api.js');
test('mock 整个 api.js模块', async () => {
expect.assertions(2);
await events.getList();
expect(fetch.getProductList).toHaveBeenCalled();
expect(fetch.getProductList).toHaveBeenCalledTimes(1);
});
遇到的问题:
因项目是使用 vue-cli 构建的,所以这里直接使用 cli-plugin-unit-jest 插件来运行 Jest 测试。
vue add @vue/cli-plugin-unit-jest
安装之后,启动项目报错:Vue packages version mismatch,这是因为 vue 与 vue-template-compiler 版本不一致,所以这里需要修改下 vue-template-compiler 的版本,
删除依赖,重新安装,或者使用下面命令。
npm install vue-template-compiler@2.6.14
对于 vue 中的路由变化如何写测试方法
methods: {
goProductList() {
this.$router.push({
name: 'productList'
})
}
}
<!-- productList.spec.js -->
import VueRouter from 'vue-router';
import routes from '@/router/registry';
import { shallowMount, createLocalVue } from '@vue/test-utils';
const router = new VueRouter({ routes });
const localVue = createLocalVue();
localVue.use(VueRouter);
it('跳转到 productList页面', () => {
const options = {
localVue,
router
};
const wrapper = shallowMount(ProductForm, options);
router.push({
name: 'productList'
});
wrapper.vm.goProductList();
expect(router.currentRoute.path).toMatch('/productList');
})
用例规范
测试脚本都要放在 tests/unit/specs 目录下
脚本命名方式为[组件名].spec.js
测试脚本由多个 describe 组成,每个 describe 由多个 it 组成
测试脚本 describe 描述填写组件名,it 描述需要简洁清晰直观