jest单元测试

补项目的单元测试补到要吐了,真心烦 = =

疑惑,单元测试是在什么开发周期开始写的啊???感觉全部开发完了之后再补肯定不对_(:з」∠)_

才刚刚在项目执行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});