如何进行Vue单元测试?从Jest开始吧!

2,966 阅读7分钟

为什么要写单元测试

  • 减少bug,避免低级错
  • 提高项目代码质量
  • 快速定位问题
  • 减少调试时间,提交开发效率
  • 便于重构

什么是Jest

Jest 中文网: Jest 是一个令人愉快的 JavaScript 测试框架,专注于 简洁明快.这些项目都在使用 Jest: Babel, TypeScript, Node, React, Angular, Vue 等!

Jest优点

  1. Jest安装配置简单,非常容易上手,几乎是零配置的,通过npm 命令安装就可以直接运行了
  2. Jest自动集成了断言库,不需要再引入第三方的断言库
  3. Jest内置有代码覆盖率报告功能,可以在整个项目范围里收集代码覆盖率信息,包括未经受测试的文件.
  4. Jest 的测试用例是并行执行的,它会自动识别一些测试文件,只执行发生改变的文件所对应的测试,提升了测试速度
  5. Jest提供snapshot功能,通过比对 UI 代码生成的快照文件,实现对 vue 框架的自动测试
  6. jest可以生成sonarQube需要的测试覆盖率报告,可将数据上传sonar
  7. vue 官方推荐的单元测试框架,对vue框架友好,并且vue脚手架可选自定集成的单元测试框架.

Jest安装与配置

安装插件

首先我们需要安装jest需要的一些插件

  • jest: Jest
  • @vue/test-utils:Vue Test Utils 是Vue.js 官方的单元测试实用工具库
  • babel-jest:使用Babel自动编译JavaScript代码
  • vue-jest:使用vue-jest去编译.vue 文件
  • jest-serializer-vue:生成vue快照的序列化器的模块,进行snapshot tests会需要
  • jest-transform-stub:处理css|图片|字体的预处理器
  • jest-sonar-reporter(可选):Jest的自定义结果处理器.处理器将Jest的输出转换为Sonar的通用测试数据格式.

配置jest.config.js

module.exports = {
  verbose: true,
  bail: 1,
  moduleFileExtensions: [
    'vue',
    'js',
    'json'
  ],
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  transform: {
    '.+\\.(css|less|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
    '^.+\\.js$': 'babel-jest',
    '.*\\.vue$': 'vue-jest',
    '^.+\\.svg$': 'jest-svg-sprite-loader'
  },
  snapshotSerializers: [ 'jest-serializer-vue' ],
  testResultsProcessor: 'jest-sonar-reporter',
  collectCoverage: true,

  collectCoverageFrom: [
    'src/**/*.{js,vue}',
  ],
  coverageReporters: ['html', 'lcov', 'text-summary'],

  coverageDirectory: './test/coverage',
  coveragePathIgnorePatterns: [ '/node_modules/' ],
  coverageThreshold: {
    global: {
      branches: 20,
      functions: 20,
      lines: 20,
      statements: 20
    }
  },
  testMatch: [
    '**/*.spec.js'
  ],
};

  • verbose: 多于一个测试文件运行时展示每个测试用例测试通过情况
  • bail: 参数指定只要有一个测试用例没有通过,就停止执行后面的测试用例
  • moduleFileExtensions: jest 需要检测测的文件类型
  • moduleNameMapper: 从正则表达式到模块名称的映射,和webpack的alisa类似
  • transform: 预处理器配置
  • snapshotSerializers: Jest在快照测试中使用的快照序列化程序模块的路径列表
  • testResultsProcessor: 自定义结果处理器,用jest-sonar-reporter输出sonar需要的通用测试数据格式
  • collectCoverage: 是否进行覆盖率收集
  • collectCoverageFrom: 需要进行收集覆盖率的文件,会依次进行执行符合的文件
  • coverageReporters: Jest在编写覆盖率报告的配置,添加"text"或"text-summary"在控制台输出中查看覆盖率摘要
  • coverageDirectory: Jest输出覆盖信息文件的目录
  • coveragePathIgnorePatterns: 需要跳过覆盖率信息收集的文件目录
  • coverageThreshold: 覆盖结果的最低阈值设置,如果未达到阈值,jest将返回失败
  • testMatch: Jest用于检测测试的文件,可以用正则去匹配

Jest钩子函数

describe / test
describe,test代表一个执行块(作用域),可以通过describe块来将测试分组,如果没有describe,那整个文件就是一个describe.

执行顺序
Jest 会在执行具体的test块之前执行所有的describe处理器部分.

describe("unit test", ()=>{
	it('10 is 10', ()=>{
		expect(10).toBe(10)
	})
	test('A is A',()=>{
		expect('A').toBe('A');
	})
})

  • jest 可以支持beforeAll,afterAll,beforeEachafterEach来编写初始化代码.
  • 默认情况下,beforeafter 的块可以应用到文件中的每个测试. 此外可以通过 describe 块来将测试分组.
  • beforeafter 的块在 describe 块内部时,则其只适用于该 describe 块内的测试.
  • 执行顺序的时候,是先执行 describe,然后在执行各个勾子函数的.
beforeAll(() => console.log('外 - beforeAll')) // 1
afterAll(() => console.log('外 - afterAll')) // 12
beforeEach(() => console.log('外 - beforeEach')) // 2 6
afterEach(() => console.log('外 - afterEach')) // 4 10
test('', () => console.log('外 - test')) // 3

describe('describe inside', () => {
    beforeAll(() => console.log('内 - beforeAll')) // 5
    afterAll(() => console.log('内 - afterAll')) // 11
    beforeEach(() => console.log('内 - beforeEach')) // 7
    afterEach(() => console.log('内 - afterEach')) // 9
    test('', () => console.log('内 - test')) // 8
})

Jest snapshot test

jest可以对整体的html进行测试有没有更改,方法就是Snapshot测试(快照测试),目前来说是jest专有的测试方法,通过对比前后的快照,可以很快找出UI的变化之处.

  • wrapper.html()获取整个DOM
  • toMatchSnapshot()第一次运行快照测试时会生成一个快照文件,之后每次执行测试的时候,会生成一个快照,然后对比最初生成的快照文件,如果没有发生改变,则通过测试;否则测试不通过,同时会输出结果,对比不匹配的地方.
  • 代码发生变化的时候,通过 控制台 按 u 来更新 快照
  • 多处 test case 更改的时候,按 i 来交互式的 更新快照
import { shallowMount } from '@vue/test-utils'
import Demo from '@/com/Demo.vue';
it('template内DOM结构的snapshot测试', () => {
     const wrapper = shallowMount(Demo)
     expect(wrapper.html()).toMatchSnapshot()
     wrapper.destroy()
})

Jest Mock

Jest 中的三个与 Mock 函数相关的API,分别是jest.fn(),jest.spyOn(),jest.mock()

jest.fn()

  • jest.fn()是创建Mock函数最简单的方式,如果没有定义函数内部的实现,jest.fn()会返回undefined作为返回值
  • jest.fn()所创建的Mock函数还可以设置返回值,定义内部实现或返回Promise对象
import { ajaxMock } from '../../__mock__/ajax.mock';
describe('ajax_test', () => {
    it('ajax_是否被调用', async () => {
        const mockFun1 = jest.fn();
        mockFun1.mockReturnValueOnce(456).mockReturnValueOnce(789);
        const mockFun2 = jest.fn(() => { return 456; });
        await ajaxMock(mockFun1);
        await ajaxMock(mockFun1);
        await ajaxMock(mockFun2);

        expect(mockFun1).toBeCalled(); // 被执行
        expect(mockFun1.mock.calls.length).toBe(2); // 调用次数
        expect(mockFun2.mock.results[0].value).toBe(456); // 返回结果
    });
});

jest.mock()

实际开发过程,一些请求方法可能我们在其他模块被调用的时候,并不需要进行实际的请求,可以使用jest.mock()mock整个模块

import call from '../src/call';
import ajax from '../src/ajax';

jest.mock('../src/ajax.js');

test('mock 整个 ajax.js模块', async () => {
  await call.getData();
  expect(ajax.ajaxGetData).toHaveBeenCalled();
  expect(ajax.ajaxGetData).toHaveBeenCalledTimes(1);
})

jest.spyOn()

jest.spyOn()方法同样创建一个mock函数,但是该mock函数不仅能够捕获函数的调用情况,还可以正常的执行被spy的函数.实际上,jest.spyOn()jest.fn()的语法糖,它创建了一个和被spy的函数具有相同内部代码的mock函数.

import call from '../src/call';
import ajax from '../src/ajax';

test('使用jest.spyOn()监控ajax.ajaxGetData被正常调用', async() => {
  expect.assertions(2);
  const spyFn = jest.spyOn(ajax, 'ajaxGetData');
  await call.getData();
  expect(spyFn).toHaveBeenCalled();
  expect(spyFn).toHaveBeenCalledTimes(1);
})

Jest报告上传sonar

  • jest.config.js 添加配置
testResultsProcessor: 'jest-sonar-reporter'
  • sonar-project.properties的运行命令中添加
// xml的位置
sonar.testExecutionReportPaths=test/covrage/test-report.xml   
// lcov.info的位置
sonar.javascript.lcov.reportPaths=test/covrage/lcov.info 

Jest coverage

jest执行完会生成一个覆盖率统计表, 所有在覆盖率统计文件夹下的文件都会被检测,覆盖率指标:

  • Statements: 语句覆盖率,执行到每个语句;
  • Branches: 分支覆盖率,执行到每个if代码块;
  • Functions: 函数覆盖率,调用到程式中的每一个函数;
  • Lines: 行覆盖率, 执行到程序中的每一行

Jest常用的断言

  • toBe()----测试具体的值
  • toEqual()----测试对象类型的值
  • toBeCalled()----测试函数被调用
  • toHaveBeenCalledTimes()----测试函数被调用的次数
  • toHaveBeenCalledWith()----测试函数被调用时的参数
  • toBeNull()----结果是null
  • toBeUndefined()----结果是undefined
  • toBeDefined()----结果是defined
  • toBeTruthy()----结果是true
  • toBeFalsy()----结果是false
  • toContain()----数组匹配,检查是否包含
  • toMatch()----匹配字符型规则,支持正则
  • toBeCloseTo()----浮点数
  • toThrow()----支持字符串,浮点数,变量
  • toMatchSnapshot()----jest特有的快照测试
  • .not.toBe()----前面加上.not就是否定形式

参考资料:

关注UU

关注公众号【前端UU】,定期获取好文推荐哟~