Vue单元测试文档

  1. 我们单元测试主要是对Vue组件进行单测

    单测使用 Jest 框架, 方法库用集成jest的 Vue Test Utils

  2. 主要的单测配置文件
    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. jest框架

    JestFacebook 开发的一款 JavaScript 测试框架, 在 Facebook 内部广泛用来测试各种 JavaScript 代码。
    其官网上主要列出了以下几个特点:

(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 项目。

  1. 单个测试模块 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()
     })
  });
});


  1. 好吧,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>

  1. 单测的文件
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. 单测的总结
    (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 深渲染是可以继承

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。