美文网首页
Vue单元测试文档

Vue单元测试文档

作者: 我是一个菜鸡程序员a | 来源:发表于2019-06-25 12:49 被阅读0次
    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 深渲染是可以继承

    相关文章

      网友评论

          本文标题:Vue单元测试文档

          本文链接:https://www.haomeiwen.com/subject/plstcctx.html