-
我们单元测试主要是对Vue组件进行单测
单测使用 Jest 框架, 方法库用集成jest的 Vue Test Utils
-
主要的单测配置文件
jest.config.js
module.exports = {
moduleFileExtensions: [
'js',
'jsx',
'json',
'vue'
],
transform: {
'^.+\\.vue$': 'vue-jest',
'.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
'^.+\\.jsx?$': 'babel-jest'
},
transformIgnorePatterns: [
'/node_modules/'
],
setupFiles: [
'<rootDir>/tests/unit/config.js'
],
moduleNameMapper: {
'^@/(.*)$': '<rootDir>/src/$1'
},
snapshotSerializers: [
'jest-serializer-vue'
],
testMatch: [
'**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
],
//覆盖率报告
coverageReporters: ['text', 'text-summary', 'html'],
testURL: 'http://localhost/'
};
package.json 进行启动配置
"scripts": {
"unit": "npm run mock | vue-cli-service test:unit --coverage",
}
最终单元测试报告,会在'工程目录'/coverage/index.html目录下生成测试覆盖率的html格式报告。
(1)使用简单。使用 create-react-app 或是 react-native init 创建的项目已经默认集成了 Jest
使用Vue 直接集成即可
npm install --save-dev jest //安装
yarn add --dev jest
// package.json 配置
{
"scripts": {
"test": "jest"
}
}
(2)内置强大的断言与 mock 功能
(3)内置测试覆盖率统计功能
(4)内置 Snapshot 机制
你可以使用它测试任何 JavaScript 项目。
- 单个测试模块 describe ,更多案例可以查看 https://vue-test-utils.vuejs.org/zh/
describe("样例", () =>{
it("deep用法", () =>{
expect({a: 1}).toEqual({a: 1});
expect(1).toBe(1);
});
});
//skip语法可以跳过测试,不用大规模注释代码
describe.skip('skip', () => {
let foo = false;
it('slip是否执行', () =>{
expect(foo).toBe(false);
});
});
//异步测试
describe('异步测试', () =>{
it('the data is peanut butter', async (done) => {
const data = await fetchData();
expect(data).toBe('peanut butter');
});
});
//Vue也提供了一个异步机制
//异步测试
describe('异步测试', () =>{
it('the data is peanut butter', async (done) => {
wrapper.vm.$nextTick(() => {
expect(wrapper.vm.value).toBe('value')
done()
})
});
});
- 好吧,Let's do it!
需要单测的 manager.vue
<ul class="dataClearList" :style="{height:mainContentHeight-68+'px'}">
<li v-for="cur in list"
class="centerPicList"
@click="managerType.type == 'managerList' &&
enterInto(cur.ci.id,cur.attrs._DCTYPE_)">
<div class="img">
<img :src="cur.attrs.PICURL|| defaultImg " :height="180" :width="306">
</div>
<div class="content">{{cur.attrs._NAME_}}</div>
<div v-if="managerType.type == 'manager'" class="widget" >
<p class="widget_edit" :title="tiModify" @click="editor(cur)"><span></span></p>
<p class="widget_delete" :title="tiDelete" @click="deleteScene(cur)"><span></span></p>
</div>
<div
v-else-if="managerType.type == 'managerList'
&& cur.attrs._DCTYPE_ && cur.attrs._DCTYPE_.length > 5"
class="widget">
<p @click.stop="enterInto(cur.ci.id,cur.attrs._DCTYPE_.substr(0, 3))">
<span class="T3D-model-a-glay">
<i class="icon ts ts-3d" style="color: #ffffff;font-size: 16px"></i>
</span>
</p>
<p @click.stop="enterInto(cur.ci.id,cur.attrs._DCTYPE_.substr(3))">
<span class="T3D-model-a-glay" >
<i class="icon ts ts-friend-circle" style="color: #ffffff;font-size: 16px"></i></span>
</p>
</div>
</li>
<li v-if="managerType.type == 'manager'" class="last" @click="editor()">
<div class="add_data">
<i class="ts ts-add"></i>
</div>
</li>
</ul>
- 单测的文件
import {shallowMount,createLocalVue } from '@vue/test-utils';
import Vue from 'vue';
import VueI18n from '../../src/js/i18n';
import Vuex from 'vuex'
import VueRouter from 'vue-router'
import Manager from '@/components/Manager.vue';
//组件化
import { Page } from 'iview';
//vuex
import fitView from '@/js/store/modules/fitView';
import getters from '@/js/store/getters';
const localVue = createLocalVue()
localVue.use(Vuex)
//router
localVue.use(VueRouter);
const router = new VueRouter()
Vue.use(VueI18n);
Vue.component('Page', Page);
describe('Manager', () => {
const elType = {
type:'manager'
};
window.IS_MOCK = true;
const store = new Vuex.Store({
state: {},
mutations: {},
modules: {
fitView
},
getters
});
const wrapper = shallowMount(Manager, {
propsData: {
managerType: elType
},
localVue,
store
});
store.commit('fitView/setMainHeight',500);
it('init manager', () => {
expect(wrapper.props().managerType).toEqual(elType);
expect(wrapper.vm.list.length).toBe(0);
expect(wrapper.vm.mainContentHeight).toBe(fitView.state.mainContent.height);
});
it('是否生成列表 模拟数据', async (done) => {
interval(function () {
expect(wrapper.vm.list.length).toBe(1)
}, function () {
return wrapper.vm.list.length>0
}, done)
})
it('数据中心 点击进入场景', async(done) => {
interval(function () {
expect(wrapper.vm.list.length).toBe(1)
}, function () {
return wrapper.vm.list.length>0
}, done);
expect(wrapper.vm.list[0].attrs._DCTYPE_.length).toBe(5);
})
const elListType = {
type:'managerList'
};
it('数据中心列表 编辑', async(done) => {
let wrappers = shallowMount(Manager, {
propsData: {
managerType: elListType
},
localVue,
store,
router
});
interval(function () {
expect(wrappers.vm.list.length).toBe(1)
}, function () {
return wrappers.vm.list.length>0
}, done);
wrapper.findAll('p').at(0).trigger('click');
expect(wrapper.vm.$router.history.router.history.current.path).toBe('/manager/editor');
})
});
- 单测的总结
(1)Vuex的引入
为了完成Vuex单测,我们需要在浅渲染组件时给 Vue 传递一个伪造的 store。
为了不污染全局的Vue,我引入了localVue 因为这是Vue独立作用域。
import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'
const localVue = createLocalVue()
localVue.use(Vuex)
(2)Router的引入
localVue 引入Router 只会暴露 route 和 router并且是只读属性
import { shallowMount, createLocalVue } from '@vue/test-utils'
import VueRouter from 'vue-router'
const localVue = createLocalVue()
localVue.use(VueRouter)
(3)mock接口的返回数据
通过mock.js的配置,将会在本地启动了一个服务端口为5002
const map = require('../tests/data'); 引入mock数据
const compression = require('compression');
const bodyParser = require('body-parser');
const map = require('../tests/data');
// 创建 application/x-www-form-urlencoded 编码解析
const urlencodedParser = bodyParser.urlencoded({extended: false});
const port = 5002;
const api = '/dcv-api/';
const app = express();
// app.use(bodyParser.json());
app.use(urlencodedParser);
app.use(compression());
app.all('*', function (req, res, next) {
// res.header('Access-Control-Allow-Origin', 'http://localhost:5000');
res.header('Access-Control-Allow-Origin', '*');
res.header('Access-Control-Allow-Headers', '*');
res.header('Access-Control-Allow-Methods', '*');
// res.header('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
if (req.method === 'OPTIONS') {
res.sendStatus(200);
} else {
next();
}
});
/**
* 修正格式
*/
const format = (result) => {
if (typeof result !== 'object' || result.success === undefined) {
result = {
success: true,
data: result
};
}
return result;
};
const start = () => {
for (let url in map) {
let result = map[url];
app.get(`${api}${url}`, (req, res) => {
res.send(format(result));
});
app.post(`${api}${url}`, (req, res) => {
console.log(url, result);
if (typeof result === 'function') {
res.send(result.call(null, req.body));
return;
}
res.send(format(result));
});
}
app.get('/', (req, res) => res.send('Hello mock!'));
};
start();
app.listen(port, () => {
console.info(`server started in http://localhost:${port}`);
});
单个的mock数据 路径tests/data/login.mock.js
/**
* 获取用户信息
*/
/**
* 登陆
*/
const login = function () {
return {
'code': 'SUCCESS',
'success': true,
'message': '登录成功',
'token': 'c6559a36b7bdb354e717ded1d559c39d3bf5a1f68498274e979d0505b98509d7ead1e9345e2b217c684bb521eec6c1b7d093acb2da81ca684c89cc0f7901be8f'
};
};
/**
* 登出
*/
const logout = function () {
const result = {
user: 'admin'
};
return result;
};
/**
* 登陆
*/
const getPicture = function () {
const result = {
'loginLogo': '',
'loginBdImg': '',
'loginBdText': ''
};
return result;
};
module.exports = {
'user/oauth/login': login(),
'user/oauth/logout': logout(),
'user/oauth/getPicture': getPicture()
};
通过tests/data/index.js 对全局的数据进行配置
let login = require('./login.mock');
let userInfo = require('./userInfo.mock');
let license = require('./license.mock');
let manager = require('./manager.mock');
var merge = function (map, map2) {
for (var key in map2) {
if (map[key]) {
console.error(`the key ${key} is used!`);
continue;
}
map[key] = map2[key];
}
return map;
};
merge(login, userInfo);
merge(login, license);
merge(login, manager);
module.exports = login;
通过对ajax进行全局数据接口拦截 window.IS_MOCK ,将调用mockUrl,是上面启动的5002端口模拟数据服务,正常是服务器请求地址
mockUrl: `http://localhost:5002${api}`,
(4)单元测试异步数据的处理
interval 异步方法,主要解决不同浏览器异步接口返回的时间不同
global.interval = function (fun,func2, done, time=10) {
let id = setInterval(function () {
if(func2()){
clearInterval(id);
fun();
done();
}
}, time);
};
describe('Manager', () => {
it('是否生成列表 模拟数据', async (done) => {
interval(function () {
expect(wrapper.vm.list.length).toBe(2);
}, function () {
return wrapper.vm.list.length>0
}, done)
expect(wrapper.vm.list[0].attrs._DCTYPE_.length).toBe(5);
})
})
(5)深浅拷贝(单元测试如果会用到i-view组件的方法)
shallowMount 浅渲染将不进行i-view继承
mount 深渲染是可以继承
网友评论