背景:
因为近期在做Jest相关的单元测试的编写,手上负责的项目又是老项目的技术架构,在此过程中遇到了很多的问题特此在这里记录一下,一是为了留个记录方便之后查询,二是让大家也帮忙校正解决的方法是否正确,好了话不多说直接开始!
相关技术栈
"nuxt": "^2.15.7",
"babel-jest": "27.4.4",
"jest": "27.4.4",
"jest-transform-stub": "^2.0.0",
"vue-jest": "3.0.4",
"@vue/test-utils": "1.3.0",
"babel-core": "7.0.0-bridge.0",
Tips: 需要注意的是本文使用的是@vue/test-utils(v1)版本哦~
准备工作:
配置jest.config.js文件
module.exports = {
coverageDirectory: '.coverage',
testMatch: ['**/__tests__/specs/*.js'],
testEnvironment: 'jsdom',
collectCoverageFrom: ['**/demo/*.vue'],
moduleFileExtensions: ['vue', 'js'],
transform: {
'^.+\\.js$': 'babel-jest',
'.*\\.(vue)$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|png|jpg|ttf|woff|woff2)$':
'jest-transform-stub',
},
coveragePathIgnorePatterns: ['/node_modules/', 'package.json', 'yarn.lock'],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1',
'@pages/(.*)$': '<rootDir>/src/pages/$1',
},
coverageThreshold: {
global: {
branches: 100,
},
},
}
以上的配置,文档说的很清楚这里就不多做解释了。
Tips:**:匹配0到多个子目录,递归匹配子目录
配置.babelrc文件
{
"env": {
"test": {
"presets": [
[
"@babel/preset-env",
{
"targets": {
"node": "current"
}
}
]
]
}
}
}
测试文件:src/pages/demo/index.vue
<template>
<div>hello jest</div>
</template>
<script>
export default {
name: 'demo',
components: {},
data() {
return {}
},
computed: {},
created() {},
mounted() {},
beforeDestroy() {},
methods: {},
}
</script>
单元测试文件:src/__tests__/specs/demo.specs.js
import Demo from '@pages/demo'
import { shallowMount } from '@vue/test-utils'
describe('demo.vue', () => {
it('测试开始', async () => {
const wrapper = shallowMount(Demo)
window.console.log(wrapper)
})
})
新增命令:package.json
"scripts": {
...
"test-watch": "jest --watchAll"
},
准备工作完成
执行
yarn test-watch or npm run test-watch
【例子1】组件挂载vuex
index.vue
<template>
<div :class="displayModeClassName">
<h2 v-if="title">{{ title }}</h2>
<div class="box" v-if="isMobile">这是H5</div>
<div class="box" v-else>这是PC</div>
<button @click="handleClick">测试按钮</button>
</div>
</template>
<script>
import { mapMutations, mapState, mapGetters } from 'vuex'
export default {
name: 'demo',
components: {},
data() {
return {}
},
computed: {
...mapState(['isMobile', 'title']),
...mapGetters(['displayModeClassName']),
},
created() {},
mounted() {},
beforeDestroy() {},
methods: {
...mapMutations(['setTitle']),
handleClick() {
this.setTitle('hello world')
},
},
}
</script>
demo.specs.js
import Vuex from 'vuex'
import Demo from '@pages/demo'
import { shallowMount, createLocalVue } from '@vue/test-utils'
let store, config
const localVue = createLocalVue()
localVue.use(Vuex)
beforeEach(() => {
store = new Vuex.Store({
state: {
isMobile: true,
title: '',
},
mutations: {
setTitle(state, name) {
state.title = name
},
setMobile(state, name) {
state.isMobile = name
},
},
getters: {
displayModeClassName: (state) => {
if (state.isMobile) {
return 'mobile'
}
return 'desktop'
},
},
})
config = {
store,
localVue,
}
})
describe('测试vuex', () => {
it('测试标题', async () => {
const wrapper = shallowMount(Demo, config)
store.commit('setTitle', 'hello jest')
await wrapper.vm.$nextTick()
const aTitle = wrapper.find('h2')
expect(aTitle.exists()).toBe(true)
expect(aTitle.text()).toBe('hello jest')
})
it('测试分辨率', async () => {
const wrapper = shallowMount(Demo, config)
store.commit('setMobile', false)
await wrapper.vm.$nextTick()
const aBox = wrapper.find('.box')
expect(aBox.exists()).toBe(true)
expect(aBox.text().indexOf('PC') !== -1).toBe(true)
})
it('测试事件', async () => {
const wrapper = shallowMount(Demo, config)
await wrapper.vm.$nextTick()
const aBox = wrapper.find('button')
await aBox.trigger('click')
const aTitle = wrapper.find('h2')
expect(aTitle.exists()).toBe(true)
expect(aTitle.text()).toBe('hello world')
})
})
如果vuex中有modules的话原理相同,参考以下代码
demo.specs.js
...
const modules = {
detail: {
namespaced: true,
state: {
...
},
mutations: {
...
},
},
}
beforeEach(() => {
store = new Vuex.Store({
modules,
state: {
isMobile: true,
title: '',
},
...
})
})
...
具体的我就不做演示了哈
【例子2】挂载全局变量
通常我们在开发组件的时候避免不了引入多语言类似于@nuxtjs/i18n这样插件它会默认挂载到vue.prototype中,然后在组件中使用this.$t(xxx)的形式,其他就是我们也会自定义的一些原型方法或者变量等,挂载到vue.prototype中,下面演示如何实现挂载。
详情可以查看mocks属性。
yarn add @nuxtjs/i18n -S or npm install @nuxtjs/i18n -S
注入自定义的全局变量
src/plugins/globalVar.js
import Vue from 'vue'
Vue.prototype.$__CONFIG__ = {
info: '我是标题',
}
Vue.prototype.$__CONFIG__FUNC = () => [1, 2, 3]
nuxt.config.js
plugins: [
{ src: '~/plugins/game-config.js', mode: 'client' },
],
index.vue
<template>
<div>
<h2>{{ $t('common.title') }}</h2>
<p>{{ $__CONFIG__.info }}</p>
<div class="item" v-for="(item, index) in rnderArrs" :key="index">
{{ item }}
</div>
</div>
</template>
<script>
export default {
name: 'demo',
data() {
return {}
},
computed: {
rnderArrs() {
return this.$__CONFIG__FUNC()
},
}
}
</script>
demo.specs.js
import Demo from '@pages/demo'
import { shallowMount, createLocalVue } from '@vue/test-utils'
let store, config, mocks
const localVue = createLocalVue()
// warn: 仅供模拟、以实际项目为主
const i18n = {
zh_CN: {
common: {
title: '测试',
},
},
}
beforeEach(() => {
mocks = {
$t: (key) => {
const keys = key.split('.')
return i18n['zh_CN'][keys[0]][keys[1]]
},
$__CONFIG__: {
info: 'jest',
},
$__CONFIG__FUNC: () => [4, 5, 6],
}
config = {
store,
localVue,
mocks,
}
})
describe('测试vue原型链', () => {
it('测试 i18n', async () => {
const wrapper = shallowMount(Demo, config)
await wrapper.vm.$nextTick()
const oTitle = wrapper.find('h2')
expect(oTitle.text()).toBe('测试')
})
it('测试 自定义变量', async () => {
const wrapper = shallowMount(Demo, config)
await wrapper.vm.$nextTick()
const oInfo = wrapper.find('p')
expect(oInfo.text()).toBe('jest')
})
it('测试 自定义变量方法', async () => {
const wrapper = shallowMount(Demo, config)
await wrapper.vm.$nextTick()
const oInfo = wrapper.findAll('.item')
expect(oInfo.length).toBe(3)
})
})
【例子3】挂载三方UI组件库
以vant2.x 为例,导入的方式无外乎两种:全部导入、按需导入,在单元测试中跟vue使用是一样的,需要那种方式就use那种即可
index.vue
<template>
<div>
<van-field v-model="value" label="文本" placeholder="请输入用户名" />
</div>
</template>
<script>
import { Field } from 'vant'
import Vue from 'vue'
Vue.use(Field)
...
</script>
demo.specs.js
import { shallowMount, createLocalVue } from '@vue/test-utils'
...
// 全部导入
// import vant from 'vant'
// const localVue = createLocalVue()
// localVue.use(vant)
import { Field } from 'vant' // 按需导入
let store, config
const localVue = createLocalVue()
localVue.use(Field)
...
【例子4】挂载自定义指令
通常在开发中关于指令要么是自己写,或者引入三方就拿vue-lazyload为例演示
index.vue
<template>
<div>
<img alt="" v-lazy="require(`@/assets/images/demo.png`)" />
</div>
</template>
...
<script>
demo.specs.js
import Demo from '@pages/demo'
import { shallowMount, createLocalVue } from '@vue/test-utils'
let store, config
const localVue = createLocalVue()
...
const lazy = jest.fn()
localVue.directive('lazy', lazy)
...
【例子5】jest.mock模板文件的方法
很多的时候我们在组件中调用的请求方法或者utils方法都统一个文件中,但是实际得到方法的返回值可能依赖其他模板各个方法的嵌套等,这个时候我们可以用jest.mock整个模板对对应方法进行返回,如下:
utils.js
export const foo = () => {
return [1, 2, 3]
}
export const sortfoo = () => {
return foo().sort()
}
export default {
foo,
sortfoo,
}
index.vue
<template>
<div>
<ul>
<li v-for="item in arrss" :key="item">{{ item }}</li>
</ul>
</div>
</template>
<script>
import { foo } from './utils' // 1、2、3
export default {
name: 'demo',
components: {},
data() {
return {}
},
computed: {
arrss() {
return foo()
},
},
...
}
</script>
demo.specs.js
import Demo from '@pages/demo'
import { shallowMount, createLocalVue } from '@vue/test-utils'
import { jest } from '@jest/globals'
let store, config
const localVue = createLocalVue()
// 对于整个模板进行mock、然后对模板的对应方法特殊处理即可
jest.mock('@pages/demo/utils', () => ({
foo: () => {
return [4, 5, 6]
},
}))
beforeEach(() => {
config = {
store,
localVue,
}
})
describe('11', () => {
it('测试 222', async () => {
const wrapper = shallowMount(Demo, config)
console.log(wrapper.text()) // 4、5、6
})
})
以上的方法会改变到vue真实组件的数据,
尝试过使用jest.spyOn和jest.fn但是不能满足我的需求,且spyOn和fn的使用场景翻阅了很多的资料发现网上的资料都是一样的,没有卵用有知道的小伙伴下面留言考诉我,感谢~
【例子6】单元覆盖率 测试commitlint拦截
提交commit的时候依赖于husky、lint-staged
yarn add lint-staged husky -D
配置package.json
"scripts": {
...
"test-watch": "jest --watchAll",
"test": "jest",
"coverage": "npm run test -- --coverage --watchAll=false || exit 0"
},
...
"devDependencies":{
"husky": "^8.0.1",
"lint-staged": "^13.0.3",
},
"lint-staged": {
"*.{js,vue}": [
"npm run coverage"
]
}
增加hook钩子
npx husky add .husky/pre-commit "npx lint-staged"
然后我们随便写一些报错的单元测试,就会出现以下截图:
最后
关于以上的关于jest的问题也会在开发项目中不定期更新,如果能帮助到大家,省去了翻阅资料的时间那就非常有价值了感谢~