Vue Test Utils/Jest单元测试

一、JEST简介

Jest是Facebook开源的一套JavaScript测试框架, 它集成了断言、JSDom、覆盖率报告等开发者所需要的所有测试工具。

二、搭建基于 jest 的 vue 单元测试环境

Vue Test Utils文档

1、安装依赖

npm install babel-jest @vue/test-utils @vue/cli-plugin-unit-jest -D
复制代码

 "devDependencies": {
    "@vue/cli-plugin-babel": "^3.12.0",
    "@vue/cli-plugin-unit-jest": "^3.12.0",
    "@vue/cli-service": "^3.12.0",
    "@vue/test-utils": "1.0.0-beta.29",
    "babel-jest": "^23.6.0",
    "babel-preset-stage-2": "^6.24.1",
    "sinon": "^11.1.1",
  },
复制代码

2、编写 jest 配置文件

//jest.conf.js   与vue.config.js同级
module.exports = {
    verbose: true,
    moduleFileExtensions: ['js', 'jsx', 'json', 'vue'],
    transform: {
        '^.+\.vue$': 'vue-jest',
        '.+\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$':
            'jest-transform-stub',
        // 为 Jest 配置 Babel
        "^.+\.js$": "<rootDir>/node_modules/babel-jest"
    },
    transformIgnorePatterns: ['/node_modules/'],
    // 别名
    moduleNameMapper: {
        "^@/(.*)$": "<rootDir>/src/$1"
    },
    snapshotSerializers: ['jest-serializer-vue'],
    testMatch: [
        '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
    ],
    testURL: 'http://localhost/',
    watchPlugins: [
        'jest-watch-typeahead/filename',
        'jest-watch-typeahead/testname'
    ],
    coverageDirectory: '<rootDir>/tests/unit/coverage', // 覆盖率报告的目录
    // collectCoverage: true, // 代码覆盖率的指标
    collectCoverageFrom: [ // 测试报告想要覆盖那些文件,目录,前面加!是避开这些文件
        // 'src/components/**/*.(js|vue)',
        'src/components/*.(vue)',
        '!src/main.js',
        '!src/router/index.js',
        '!**/node_modules/**'
    ]
};
复制代码

3、编写启动文件 setup.js

放置一些全局定义和引入

// ./test/setup.js 
import Vue from 'vue';

// 将Vue暴露到全局里面
// global.Vue = Vue;
// console.log('--global:',global.hasOwnProperty('Vue'))
Vue.config.productionTip = false;
// 使用elementUi组件
import ElementUI from 'element-ui';
// npm run unit 时要下面引入样式那句注释掉-不知为什么导入会报错。可能因为测试时,不需要css样式
// import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);

// 注册全局组件
import localModules from '@/modules'
Object.keys(localModules).map(key => {
    Vue.component(key, localModules[key])
});
复制代码

在jest.conf.js文件中加入下一行

module.exports = {
	...,
  setupFiles: ['<rootDir>/tests/setup']
}
复制代码

4、加入启动 jest 的 npm script

"scripts": {
  "test:unit": "vue-cli-service test:unit",
  "unitc": "vue-cli-service test:unit --coverage"
},
复制代码
--coverage 生成测试覆盖率
--watch  单文件监视测试
--watchAll  监视所有文件改动,测试相应的测试。
复制代码

三、踩坑集

使用elementui模块, 报错如下:

解决方案:
第一步:新建setup.js文件
//test/unit/setup.js
import Vue from 'vue';

// 将Vue暴露到全局里面
// global.Vue = Vue;
// console.log('--global:',global.hasOwnProperty('Vue'))
Vue.config.productionTip = false;

// 使用elementUi组件
import ElementUI from 'element-ui';
// npm run unit 时要下面引入样式那句注释掉-不知为什么导入会报错。可能因为测试时,不需要css样式
// import 'element-ui/lib/theme-chalk/index.css';
Vue.use(ElementUI);
复制代码
第二步:在 jest.conf.js或jest 配置下新增setupFiles
// jest.conf.js
'setupFiles': ['<rootDir>/tests/setup']
复制代码

无法识别es6语法

// .babelrc配置
{
  "plugins": [
    "transform-es2015-modules-commonjs"
  ]
}
复制代码

引用Vuex报错

...mapState解析不出

// 安装 npm install babel-preset-stage-2
// 修改 .babelrc配置文件
{
  "plugins": [
    "transform-es2015-modules-commonjs","transform-object-rest-spread"
  ],
}
复制代码

四、使用

1、编写第一个测试文件

// ./src/components/hello-world.vue
<template>
  <div>
    <h1>{{ msg }}</h1>
  </div>
</template>

<script>
export default {
  name: 'HelloWorld',
  data() {
    return {
      msg: 'Hello Jest',
    };
  },
};
</script>
复制代码

对该组件进行测试

// ./test/unit/hello-world.spec.js
import { shallowMount } from '@vue/test-utils';
import HelloWorld from '@/components/hello-world';

describe('<hello-world/>', () => {
  it('should render correct contents', () => {
    const wrapper = shallowMount(HelloWorld);
    expect(wrapper.find('.hello h1').text())
      .toEqual('Welcome to Your Vue.js App');
  });
});
复制代码

启动测试

npm run test:unit
复制代码

2、测试Dom事件

import {mount, shallowMount, createLocalVue} from "@vue/test-utils";
import networkDiagnosis from "@/views/Manage/device/networkDiagnosis";
import sinon from "sinon";
import Vuex from 'vuex';
const localVue = createLocalVue();
localVue.use(Vuex);

describe("networkDiagnosis网络诊断管理 —— 表单参数", () => {  // describe 代表一个作用域
    const clickHandler = sinon.stub();

    it('测试网络诊断Ping类型下没有ip', async () => {
        // 通过 mount 生成了一个包裹器,包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法
        let wrapper = mount(networkDiagnosis, {
            listeners: {clickHandler},
            // 加stubs 参数是为了避免这个问题  https://github.com/vuejs/vue-test-utils/issues/958  Cannot read property '$el' of undefined
            stubs: {transition: false},
            localVue
        });
        wrapper.setData({
            form: {
                diagnosisType: 'Ping',
                ip: '' // 172.16.16.30
            },
        });
        // .vm 可以获取当前实例对象,相当于拿到了 vue组件里的 this 对象
        // const vm = wrapper.vm;
        const button = wrapper.find('.blueLongBtn');
        await button.trigger('click');
        expect(wrapper.findAll('.el-form-item__error').length).toBe(1)
    });
})
复制代码

3、使用axios模拟接口

现在按照下列3个步骤:

  1. 在测试文件中导入需要模拟的模块
  2. 使用jest.mock()方法mock一下模块
  3. 使用.mockResolvedValue()模拟数据返回
// index.test.js
const getFirstAlbumTitle = require('./index');
const axios = require('axios');

jest.mock('axios');

it('returns the title of the first album', async () => {
  axios.get.mockResolvedValue({
    data: [
      {
        userId: 1,
        id: 1,
        title: 'My First Album'
      },
      {
        userId: 1,
        id: 2,
        title: 'Album: The Sequel'
      }
    ]
  });
  const title = await getFirstAlbumTitle();
  expect(title).toEqual('My First Album');
});
复制代码

这里面最核心的一句是jest.mock('axios'),这让jest用一个空的函数,接管了axios的所有行为,在没有使用mockResolvedValue方法前,本文件中的axios的所有方法都将返回undefined

4、mock接口

在接口文件同一目录下,创建__mock__, 然后在__mock__文件夹下mock接口文件

具体如下:

__mocks __/device.js (在这里统一写mock函数)

export const netDiagnosisPing = jest.fn(
    (ip) =>
        new Promise((resolve, reject) => {
            resolve({
                code: 0,
                message: "ping success",
                data: [
                    "",
                    "正在 Ping 172.16.16.30 具有 32 字节的数据:",
                    "来自 172.16.16.30 的回复: 字节=32 时间<1ms TTL=63",
                    "......"
                ]
            });
        }),
);

export const netDiagnosisTelnet = jest.fn(
    (ip, port) =>
        new Promise((resolve, reject) => {
            if (ip === '172.16.16.30' && port === '8080') {
                resolve({
                    code: 0,
                    message: "telnet success",
                    data: null
                });
            } else {
                reject({
                    code: 1,
                    message: "网络诊断失败!",
                    data: null
                });
            }
        }),
);
复制代码

diagnosisRequest.spec.js (在spec文件中mock对应路径的模块即可)

import {createLocalVue, mount} from "@vue/test-utils";
import networkDiagnosis from "@/views/Manage/device/networkDiagnosis";
import {netDiagnosisPing, netDiagnosisTelnet} from "@/api/device";
import sinon from "sinon";

const localVue = createLocalVue();
jest.mock('../../../src/api/device.js');

describe("networkDiagnosis网络诊断管理 —— 表单请求", () => {
    const clickHandler = sinon.stub();
    test('Telnet类型网络诊断', async (done) => {
        // let wrapper = mount(networkDiagnosis,{localVue});
        // wrapper.setData({
        //     form: {
        //         diagnosisType: 'Telnet',
        //         ip: '172.16.16.30',
        //         port: '8080'
        //     },
        // });
        // const button = wrapper.find('.blueLongBtn');
        // await button.trigger('click');
        //
        const res = await netDiagnosisTelnet('172.16.16.30','8080');
        expect(res.code).toEqual(0);
        done();
    });
复制代码

重点:

五、扩展

mount和shallowMount

区别

  • mount会渲染整个组件树而shallowMount在挂载组件之前对所有子组件进行存根。
  • shallowMount可以确保你对一个组件进行独立测试,有助于避免测试中因子组件的渲染输出而混乱结果。

使用建议

  • mount适合小组件测试
  • shallowMount适合多场景测试(能用shallowMount就不用mount)

Sinon.js

Sinon.js是一个用来做独立测试和模拟的JavaScript库。它在单元测试的编写中通常用来模拟HTTP等相关请求。

expect.assertions

expect.assertions(number)验证在测试期间是否调用了一定数量的断言。 这在测试异步代码时通常很有用,以确保实际调用了回调中的断言。

在jest单元测试中模拟接口请求

如果不模拟接口请求,每次跑测试用例,都会真实地请求albums接口,严重拖慢测试的速度

Guess you like

Origin juejin.im/post/7074763876487102478