补项目的单元测试补到要吐了,真心烦 = =
疑惑,单元测试是在什么开发周期开始写的啊???感觉全部开发完了之后再补肯定不对_(:з」∠)_
才刚刚在项目执行npm run unit
就翻车了。(╯‵□′)╯︵┻━┻
问题
1. Option “mapCoverage” has been removed, as it’s no longer necessary.
详细信息:
Option "mapCoverage" has been removed, as it's no longer necessary.
Please update your configuration.
Configuration Documentation:
https://facebook.github.io/jest/docs/configuration.html
解决方法:
在配置文件jest.conf.js中删除mapCoverage
2. SecurityError: localStorage is not available for opaque origins
详细信息:
SecurityError: localStorage is not available for opaque origins
at Window.get localStorage [as localStorage] (node_modules/jsdom/lib/jsdom/browser/Window.js:257:15)
at Array.forEach (<anonymous>)
解决方法:
修改 jest.conf.js 文件添加一行testURL
const path = require('path')
module.exports = {
rootDir: path.resolve(__dirname, '../../'),
moduleFileExtensions: [
'js',
'json',
'vue'
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
transform: {
'^.+\\.js$': '<rootDir>/node_modules/babel-jest',
'.*\\.(vue)$': '<rootDir>/node_modules/vue-jest'
},
testPathIgnorePatterns: [
'<rootDir>/test/e2e'
],
snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
setupFiles: ['<rootDir>/test/unit/setup'],
coverageDirectory: '<rootDir>/test/unit/coverage',
collectCoverageFrom: [
'src/**/*.{js,vue}',
'!src/main.js',
'!src/router/index.js',
'!**/node_modules/**'
],
'testURL': 'http://localhost'
}
参考资料:
https://blog.csdn.net/yj1499945/article/details/88988628
3.Test suite failed to run
详细信息:
● Test suite failed to run
Your test suite must contain at least one test.
......
解决方法: 测试文件不能为空,只要有一个测试方法。
4.Uncaught TypeError: Cannot read property ‘getters’ of undefined
详细信息:
Uncaught TypeError: Cannot read property 'getters' of undefined
解决方法: 如下参考资料
参考资料: Uncaught TypeError: Cannot read property ‘getters’ of undefined
Jest配置文件
jest.conf.js 配置文件
const path = require('path')
module.exports = {
rootDir: path.resolve(__dirname, '../../'),
moduleFileExtensions: [
'js',
'json',
'vue'
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
transform: {
'^.+\\.js$': '<rootDir>/node_modules/babel-jest',
'.*\\.(vue)$': '<rootDir>/node_modules/vue-jest'
},
testPathIgnorePatterns: [
'<rootDir>/test/e2e'
],
snapshotSerializers: ['<rootDir>/node_modules/jest-serializer-vue'],
setupFiles: ['<rootDir>/test/unit/setup'],
coverageDirectory: '<rootDir>/test/unit/coverage',
// jest 覆盖的文件
collectCoverageFrom: [
//'src/**/*.{js,vue}',
'src/**/*.js',
'!src/main.js',
'!src/router/index.js',
'!src/store/index.js',
'!src/mock/*.js',
'!**/node_modules/**'
],
// 全局变量的定义
globals: {
baseUrl: 'http://intranet.cityworks.cn:88/cloud-gateway/manage-server',
urlPreview: 'http://intranet.cityworks.cn:88/cloud-gateway/preview',
exportUrl: 'http://intranet.cityworks.cn:88/cloud-gateway/export',
pagePreUrl: 'http://intranet.cityworks.cn:88/cloud-gateway/export',
monitorUrl: 'http://10.99.85.174:9123'
},
verbose: true,
// 忽略的文件
coveragePathIgnorePatterns: ["node_modules", 'iconfont.js', 'request.js', 'rightMenu.js'],
testURL: 'http://localhost'
}
setup.js 环境配置文件
import Vue from 'vue'
import store from "../../src/store";
import ElementUI from 'element-ui';
import '../../src/mock/index.js'
Vue.config.productionTip = false;
Vue.use(ElementUI);
new Vue({
store,
});
// 全局函数配置
global.console = {
log: jest.fn(),
error: jest.fn(),
warn: jest.fn(),
info: jest.fn(),
debug: jest.fn(),
};
const Message = jest.fn();
sessionStorage.setItem('username', 'test');
// 响应时间配置
jest.setTimeout(40000);
普通jest单元测试
fns.spec.js
import {isFunction} from '../../../src/views/PageEdit/pageEditContent/utils/fns.js'
describe('测试isFunction模块', () => {
test('测试isFunction模块', () => {
expect(isFunction({
offsetParent: {
clientLeft: 1,
clientTop: 1,
offsetLeft: 1,
offsetTop: 1,
},
offsetLeft: 1,
offsetTop: 1,
offsetHeight: 1,
offsetWidth: 1
})).toBe(false);
expect(isFunction(() => {
})).toBe(true);
});
});
针对 vuex 的jest单元测试
state、mutations、getters的单元测试跟普通单元测试一样
state.spec.js
test('测试文稿的state', () => {
expect(AllPage.state).toMatchObject({
pageList: expect.any(Array),
pageDetail: expect.any(Object),
pageResult: expect.any(Object),
pageLoading: expect.any(Boolean),
pageData: expect.any(Object),
});
});
mutations.spec.js
test('测试文稿的mutations', () => {
let state = {
pageList: [],
pageDetail: {},
pageResult: {result: false, id: null},
pageLoading: false,
pageData: {}
};
AllPage.mutations.SET_PAGE_LIST(state, [1, 2, 3]);
expect(state.pageList).toEqual([1, 2, 3]);
AllPage.mutations.SET_PAGE_DETAIL(state, {a: 1});
expect(state.pageDetail).toEqual({a: 1});
AllPage.mutations.SET_PAGE_RESULT(state, {result: true, id: null});
expect(state.pageResult).toEqual({result: true, id: null});
AllPage.mutations.SET_PAGE_LOADING(state, true);
expect(state.pageLoading).toEqual(true);
AllPage.mutations.SET_PAGE_DATA(state, {a: 1});
expect(state.pageData).toEqual({a: 1});
});
getters.spec.js
test('测试文稿的getters', () => {
let state = {
pageList: [1, 2, 3],
pageDetail: {a: 1},
pageResult: {result: false, id: null},
pageLoading: false,
pageData: {a: 1}
};
AllPage.getters.getShowPageList(state);
expect(state.pageList).toEqual([1, 2, 3]);
AllPage.getters.getPageDetail(state);
expect(state.pageDetail).toEqual({a: 1});
AllPage.getters.getPageResult(state);
expect(state.pageResult).toEqual({result: false, id: null});
AllPage.getters.getPageLoading(state);
expect(state.pageLoading).toEqual(false);
AllPage.getters.getPageData(state);
expect(state.pageData).toEqual({a: 1});
});
测试Actions的话,先写个模拟action的通用函数帮助测试。
// 用指定的 mutations 测试 action 的辅助函数
export const testAction = (action, args, state, expectedMutations, done, waitTime) => {
let count = 0;
// 模拟提交
const commit = (type, payload) => {
const mutation = expectedMutations[count];
try {
expect(mutation.type).toEqual(type);
if (payload) {
expect(mutation.payload).toEqual(payload)
}
} catch (error) {
done(error)
}
count++;
if (count >= expectedMutations.length) {
if (waitTime) {
setTimeout(() => {
done()
}, waitTime)
} else {
done()
}
}
};
// 用模拟的 store 和参数调用 action
action({commit, state}, ...args);
// 检查是否没有 mutation 被 dispatch
if (expectedMutations.length === 0) {
// expect(count).toEqual(0);
// done()
setTimeout(() => {
done()
}, 2000)
}
};
actions.spec.js
import {testAction} from './common.js'
test('测试获取文稿分页列表Action成功', done => {
testAction(AllPage.actions.setPageList, [{all: 1, order: ['top:desc', 'create_time:desc'].join(',')}], {}, [
{
type: 'SET_PAGE_LOADING', payload: expect.any(Boolean)
},
{
type: 'SET_PAGE_LIST', payload: expect.any(Array)
},
{
type: 'SET_PAGE_LOADING', payload: expect.any(Boolean)
},
{
type: 'SET_PAGE_DATA', payload: expect.any(Object)
}
], done, 1500)
}, 15000);
test('测试获取文稿分页列表Action失败', done => {
testAction(AllPage.actions.setPageList, [{
test: true,
order: ['i:desc'].join(',')
}], {}, [
{
type: 'SET_PAGE_LOADING', payload: expect.any(Boolean)
},
{
type: 'SET_PAGE_LOADING', payload: expect.any(Boolean)
},
], done, 1000)
}, 15000);
其他一些模拟工具
模拟键盘事件
// js模拟键盘事件
export function fireKeyEvent(el, evtType, keyCode){
var doc = el.ownerDocument,
win = doc.defaultView || doc.parentWindow,
evtObj;
if(doc.createEvent){
if(win.KeyEvent) {
evtObj = doc.createEvent('KeyEvents');
evtObj.initKeyEvent( evtType, true, true, win, false, false, false, false, keyCode, 0 );
}
else {
evtObj = doc.createEvent('UIEvents');
Object.defineProperty(evtObj, 'keyCode', {
get : function() { return this.keyCodeVal; }
});
Object.defineProperty(evtObj, 'which', {
get : function() { return this.keyCodeVal; }
});
evtObj.initUIEvent( evtType, true, true, win, 1 );
evtObj.keyCodeVal = keyCode;
if (evtObj.keyCode !== keyCode) {
console.log("keyCode " + evtObj.keyCode + " 和 (" + evtObj.which + ") 不匹配");
}
}
el.dispatchEvent(evtObj);
}
else if(doc.createEventObject){
evtObj = doc.createEventObject();
evtObj.keyCode = keyCode;
el.fireEvent('on' + evtType, evtObj);
}
}
使用
fireKeyEvent(btn, 'keydown', 17);
模拟选择dom
插入HTML
describe('测试编辑store模块的actions', () => {
document.body.innerHTML = '\
<div class="list_con">\
<div data-v-4ca57179="" id="hez85h5f3m4" style="width: 100%; height: 100%; position: relative; z-index: 998;">\
<button id="clickBtn" src="http://intranet.cityworks.cn:88/minio-dev/client-picture/2019-12-13/9c3ab423f77540afbc5c1b4ddd9c67a1.jpg" alt="" style="width: 100%; height: 100%;">点击事件\
</button>\
</div>\
</div>';
test('测试setTreeActiveId-情况5', done => {
Object.defineProperty(window.navigator, 'platform', {value: 'MacIntel', configurable: true});
let btn = document.querySelector('#clickBtn');
let ev = new KeyboardEvent('keyup', {
keyCode: 17,
ctrlKey: true
});
document.dispatchEvent(ev);
fireKeyEvent(btn, 'keydown', 17);
testAction(actions.setTreeActiveId, [], {state}, [
{
type: 'TOGGLE_TREE_ACTIVE_ID', payload: expect.any(Array)
},
], done)
});
test('测试setTreeActiveId-情况6', done => {
Object.defineProperty(window.navigator, 'platform', {value: 'MacIntel', configurable: true});
let btn = document.querySelector('#clickBtn');
let ev = new KeyboardEvent('keyup', {
keyCode: 17,
ctrlKey: true
});
document.dispatchEvent(ev);
fireKeyEvent(btn, 'keydown', '17');
testAction(actions.setTreeActiveId, [], {state}, [
{
type: 'TOGGLE_TREE_ACTIVE_ID', payload: expect.any(Array)
},
], done)
});
});
模拟不同的运行系统
Object.defineProperty(window.navigator, 'platform', {value: 'Mac68K', configurable: true});
Object.defineProperty(window.navigator, 'platform', {value: 'MacPPC', configurable: true});
Object.defineProperty(window.navigator, 'platform', {value: 'Macintosh', configurable: true});
Object.defineProperty(window.navigator, 'platform', {value: 'MacIntel', configurable: true});