jest+vue-test-utils初步实践
一、起步
1. jest
Jest是 Facebook 的一套开源的 JavaScript 测试框架, 它自动集成了断言、JSDom、覆盖率报告等开发者所需要的所有测试工具,配置较少,对vue框架友好。
2. vue-test-utils
Vue Test Utils 是 Vue.js 官方的单元测试实用工具库,为jest和vue提供了一个桥梁,暴露出一些接口,让我们更加方便的通过Jest为Vue应用编写单元测试。
3. 安装
如果已经安装配置了webpack、babel的vue脚手架,现在需要在安装的是:
npm i jest @vue/test-utils vue-jest babel-jest jest-serializer-vue -D
4. 在package.json中定义单元测试的脚本和配置jest
"scripts": { "test": "jest" },
"jest": { "moduleFileExtensions": [ "js", // 告诉 Jest 处理 `*.vue` 文件 "vue" ], "moduleNameMapper": { // 支持源代码中相同的 `@` -> `src` 别名 "^@/(.*)$": "<rootDir>/src/$1" }, "transform": { // 用 `babel-jest` 处理 `*.js` 文件 "^.+\\.js$": "<rootDir>/node_modules/babel-jest", // 用 `vue-jest` 处理 `*.vue` 文件 ".*\\.(vue)$": "<rootDir>/node_modules/vue-jest" }, "snapshotSerializers": [ // 快照的序列化工具 "<rootDir>/node_modules/jest-serializer-vue" ], //成多种格式的测试覆盖率报告 可选 "collectCoverage": true, "collectCoverageFrom": ["**/*.{js,vue}", "!**/node_modules/**"] }
5. 配置.babelrc文件
{ "presets": [ ["env", { "modules": false }] ], "env": { "test": { "presets": [ ["env"] ] } } }
6. 写测试文件,放在根目录下test文件夹中,以.spec.js或.test.js为后缀名的文件,正常名字和组件名一致,如测试header组件,应该名字叫做header.spec.js或header.test.js
header.vue
<template> <div> <div> <span class="count">{{ count }}</span> <button @click="increment">Increment</button> </div> </div> </template> <script> export default { data () { return { count: 0 } }, methods: { increment () { this.count++ } } } </script>
header.spec.js
import { mount } from '@vue/test-utils' import header from '@/components/header.vue' describe('header', () => { const wrapper = mount(header) it('点击按钮', () => { const button = wrapper.find('button'); expect(button.text()).toBe('Increment'); button.trigger('click'); expect(wrapper.find('.count').text()).toBe('1') }) })
7. 执行命令行:npm test,运行单元测试
二、jest常用API
1. 全局函数
1)beforeAll(fn, timeout)/afterAll(fn, timeout)
在所有测试运行之前/之后执行,第二个参数可选,默认为5秒
2)beforeEach(fn, timeout)/afterEach(fn, timeout)
在每个测试运行之前/之后执行,第二个参数可选,默认为5秒
3)describe(name, fn)
创建一个块,将几个相关的测试组合在一起。
describe.only(name, fn)只运行一次
4)test(name, fn, timeout)
测试方法,test(name, fn, timeout) 等价于 it(name, fn, timeout), 第三个参数可选,默认为5秒
test.only(name, fn, timeout),只运行一次
2. 匹配器
1)相等、不相等匹配
toBe
test('3+3等于6', () => { expect(3 + 3).toBe(6); });
expect(3+3)返回我们期望的结果,toBe是匹配器,匹配具体的某一个值
toEqual
如果是匹配对象,需要使用toEqual
const obj = { one: 1, two: 2 }; expect(obj).toBe({ one: 1, two: 2 });
not:匹配不相等
expect(3+3).not.toBe(6);
2)匹配真假
toBeNull 只匹配 null
toBeUndefined 只匹配 undefined
toBeDefined 与...相反 toBeUndefined
toBeTruthy匹配if声明视为真的任何内容
toBeFalsy匹配if语句视为false的任何内容
3)匹配数字
toBeGreaterThan(3) 大于3
toBeGreaterThanOrEqual(3) 等于或大于3
toBeLessThan(3) 小于3
toBeLessThanOrEqual(3) 等于或小于3
对于浮点相等,请使用toBeCloseTo
const value = 0.1 + 0.2; // expect(value).toBe(0.3); //报错 Received: 0.30000000000000004 expect(value).toBeCloseTo(0.3);
4)匹配字符串,toMatch 支持正则
test('but there is a "stop" in Christoph', () => { expect('Christoph').toMatch(/stop/); });
5)匹配数组,toContain,数组中是否包含
const shoppingList = [ 'paper towels', 'beer', ]; test('the shopping list has beer on it', () => { expect(shoppingList).toContain('beer'); });
3. 异步函数
1)回调函数
回调是异步比较常见,实现如下
function fetchData (callback) { setTimeout(() => { callback('peanut butter'); }, 1000) } test('the data is peanut butter', done => { function callback (data) { expect(data).toBe('peanut butter'); done(); } fetchData(callback); });
如果不写done(),将会报超时的错误
2)promise
function fetchData () { return new Promise((resolve, reject) => { setTimeout(() => { resolve('peanut butter') }, 1000) }) } test('the data is peanut butter', () => { expect.assertions(1); return fetchData().then(data => { expect(data).toBe('peanut butter'); }); });
assertions(1)代表的是在当前的测试中至少有一个断言是被调用的,否则判定为失败。
如果删掉return语句,那么你的测试将在fetchData完成之前结束。
配合 .resolves/.rejects使用
test('the data is peanut butter', () => { expect.assertions(1); return expect(fetchData()).resolves.toBe('peanut butter'); });
3)async/await
使用async/await可以使代码看起来更加整洁
test('the data is peanut butter', async () => { expect.assertions(1); await expect(fetchData()).resolves.toBe('peanut butter'); });
4. mock
在项目中,一个模块的方法内常常会去调用另外一个模块的方法。在单元测试中,我们可能并不需要关心内部调用的方法的执行过程和结果,只想知道它是否被正确调用即可,甚至会指定该函数的返回值。此时,使用Mock函数是十分有必要。
常用的方法有:jest.fn()/jest.mock()/jest.spyOn
1) jest.fn()
jest.fn()是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值。
test('测试jest.fn()调用', () => { let mockFn = jest.fn(); let result = mockFn(1, 2, 3); // 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().mockReturnValue('default'); // mockFn执行后返回值为default expect(mockFn()).toBe('default'); })
模拟另一个模块的调用情况
fetch.js
export default { fetch (callback) { callback() } }
fetch.test.js
import fetch from './fetch'; test('测试jest.fn()调用', () => { let mockFn = jest.fn(); fetch.fetch(mockFn); expect(mockFn).toBeCalled() })
2)jest.mock()
fetch.js文件夹中封装的请求方法可能我们在其他模块被调用的时候,并不需要进行实际的请求。这时使用jest.mock()去mock整个模块是十分有必要的
创建event.js文件,用来调用fetch.js
import fetch from './fetch';
export default { getPostList () { return fetch.fetch(data => { console.log('fetchPostsList be called!'); }); } }
fetch.test.js
import events from './event'; import fetch from './fetch';
jest.mock('./fetch.js'); test('mock 整个 fetch.js模块', () => { expect.assertions(1); events.getPostList(); expect(fetch.fetch).toHaveBeenCalled(); });
使用jest.mock('./fetch.js')模拟fetch模块,若不用jest.mock,会报错:jest.fn() value must be a mock function or spy.
3)jest.sypOn()
在上面的jest.mock()中,并没有执行console.log('fetchPostsList be called!'),说明fetch函数没有执行。jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数。
import events from './event'; import fetch from './fetch'; test('使用jest.spyOn()监控fetch.fetch被正常调用', () => { expect.assertions(1); const spyFn = jest.spyOn(fetch, 'fetch'); events.getPostList(); expect(spyFn).toHaveBeenCalled(); })
使用jest.spyOn,执行了console.log('fetchPostsList be called!')。
三、vue-test-utils常用API
1. mount
创建一个包含被挂载和渲染的 Vue 组件的 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) }) })
2.shallowMount
和 mount 一样,创建一个包含被挂载和渲染的 Vue 组件的 Wrapper,不同的是被存根的子组件
import { shallowMount } from '@vue/test-utils' import Foo from './Foo.vue' describe('Foo', () => { it('返回一个 div', () => { const wrapper = shallowMount(Foo) expect(wrapper.contains('div')).toBe(true) }) })
3.render render
在底层使用 vue-server-renderer 将一个组件渲染为静态的 HTML。
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.text()).toContain('<div></div>') }) })
4.createLocalVue
createLocalVue 返回一个 Vue 的类供你添加组件、混入和安装插件而不会污染全局的 Vue 类。
可通过 options.localVue 来使用
import { createLocalVue, shallowMount } from '@vue/test-utils' import Foo from './Foo.vue' const localVue = createLocalVue() const wrapper = shallowMount(Foo, { localVue, mocks: { foo: true } }) expect(wrapper.vm.foo).toBe(true) const freshWrapper = shallowMount(Foo) expect(freshWrapper.vm.foo).toBe(false)
5.选择器
很多方法的参数中都包含选择器。一个选择器可以是一个 CSS 选择器、一个 Vue 组件或是一个查找选项对象。
标签选择器 (div、foo、bar)
类选择器 (.foo、.bar)
特性选择器 ([foo]、[foo="bar"])
id 选择器 (#foo、#bar)
伪选择器 (div:first-of-type)
近邻兄弟选择器 (div + .foo)
一般兄弟选择器 (div ~ .foo)
const buttonr = wrapper.find('.button') const content = wrapper.find('#content')
6.伪造 $route 和 $router
有的时候你想要测试一个组件在配合 $route 和 $router 对象的参数时的行为。这时候你可以传递自定义假数据给 Vue 实例。
import { shallowMount } from '@vue/test-utils' const $route = { path: '/home' } const wrapper = shallowMount(Component, { mocks: { $route } }) expect(wrapper.vm.$route.path).toBe('/home')
7.测试vuex
在测试vuex,主要通过伪造state/getters/mutations/actions进行测试
.vue文件中
<template> <div> <h1>{{appName}}</h1> <h2>{{appNameWithVersion}}</h2> <button @click="updateAppName" class="mutation">mutation update</button> <button @click="updateActionAppName" class='action'>action update</button> </div> </template> <script> export default { methods: { updateAppName () { this.$store.commit('setAppName', 'admin2') }, updateActionAppName () { this.$store.dispatch('setName', 'admin3') } }, computed: { appName () { return this.$store.state.appName; }, appNameWithVersion () { return this.$store.getters.appNameWithVersion; } } } </script>
.spec.js文件中
import { shallowMount, createLocalVue } from '@vue/test-utils' import Vuex from 'vuex' import Header from '@/components/Header.vue' const localVue = createLocalVue() localVue.use(Vuex) describe('header.vue', () => { let wrapper; let store, state, getters, mutations, actions; beforeEach(() => { state = { appName: 'admin' }; getters = { appNameWithVersion: () => 'getters' } mutations = { setAppName: jest.fn(), } actions = { setName: jest.fn(), } store = new Vuex.Store({ state, getters, mutations, actions }) wrapper = shallowMount(Header, { store, localVue }) }) it('测试vuex', () => { expect(wrapper.find('h1').text()).toBe(state.appName) expect(wrapper.find('h2').text()).toBe(getters.appNameWithVersion()) wrapper.find('.mutation').trigger('click') expect(mutations.setAppName).toHaveBeenCalled() wrapper.find('.action').trigger('click') expect(actions.setName).toHaveBeenCalled() }) })
完...
参考:
1.https://vue-test-utils.vuejs.org/zh/
2.https://jestjs.io/docs/en/getting-started
3.http://www.cnblogs.com/Wolfmanlq/p/8018370.html
4.https://www.jianshu.com/p/70a4f026a0f1
5.https://www.jianshu.com/p/ad87eaf54622