阅读 133

vue 项目增加 Jest 单元测试

单元测试好处

  • 提供描述组件行为的文档
  • 节省手动测试的时间
  • 减少研发新特性时产生的 bug
  • 改进设计
  • 促进重构

自动化测试使得大团队中的开发者可以维护复杂的基础代码。

TDD & BDD

TDD(Test Driven Development)测试驱动开发

TDD 的思想是根据需求先写测试用例,依照测试用例再去写功能代码。当增加或者修改某一项需求的时候,需要先修改测试用例,再依照测试用例去修改代码逻辑。

基本步骤

  1. 编写测试用例
  2. 运行测试,测试用例无法通过测试
  3. 编写代码,使测试用例通过测试
  4. 优化代码,完成开发
  5. 重复以上步骤

BDD(Behavior Driven Development)行为驱动开发

与 TDD 相反,BDD 是根据需求先进行开发,等到该功能开发完毕后,再开始编写测试代码进行测试。

基本步骤

  1. 编写代码
  2. 编写测试用例,测试无法通过
  3. 编写代码,使测试用例通过
  4. 优化代码,完成开发
  5. 重复以上步骤

目前我们采用的是BDD模式。

Jest配置

如果是使用vue-cli构建的项目,在初始化时会询问是否使用单元测试,只需按步骤选择jest即可,会自动安装Vue Test Utils,它是 Vue.js 官方的单元测试实用工具库,为 jestVue 提供了一个桥梁,暴露出一些接口,让我们更加方便的通过 JestVue 应用编写单元测试。

在生成的项目根目录下,会有一个 jest.config.json 文件,cli自动生成所用的预设是@vue/cli-plugin-unit-jest。我们可以在这里对 jest 进行个性化的配置。以下是一个配置文档的例子。配置文档具体参数说明

module.exports = {
    // 告诉jest需要解析的文件
    moduleFileExtensions: [
        'js',
        'jsx',
        'json',
        'vue'
    ],
    // 告诉jest去哪里找模块资源,同webpack中的modules
    moduleDirectories: [
        'src',
        'node_modules'
    ],
    // 告诉jest针对不同类型的文件如何转义
    transform: {
        '^.+\\.(vue)$': '<rootDir>/node_modules/vue-jest',
        '^.+\\.js$': '<rootDir>/node_modules/babel-jest',
        '.+\\.(css|styl|less|sass|scss|svg|png|jpg|ttf|woff|woff2)$': 'jest-transform-stub',
        '^.+\\.jsx?$': 'babel-jest',
        '^.+\\.ts?$': 'ts-jest'
    },
    // 告诉jest在编辑的过程中可以忽略哪些文件,默认为node_modules下的所有文件
    transformIgnorePatterns: [
        '<rootDir>/node_modules/'
        + '(?!(vue-awesome|veui|resize-detector|froala-editor|echarts|html2canvas|jspdf))'
    ],
    // 别名,同webpack中的alias
    moduleNameMapper: {
        '^src(.*)$': '<rootDir>/src/$1',
        '^@/(.*)$': '<rootDir>/src/$1',
        '^block(.*)$': '<rootDir>/src/components/block/$1',
        '^toolkit(.*)$': '<rootDir>/src/components/toolkit/$1'
    },
    snapshotSerializers: [
        'jest-serializer-vue'
    ],
    // 告诉jest去哪里找我们编写的测试文件
    testMatch: [
        // '**/tests/unit/**/*.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
        '**/tests/unit/**/Test.spec.(js|jsx|ts|tsx)|**/__tests__/*.(js|jsx|ts|tsx)'
    ],
    // 在执行测试用例之前需要先执行的文件
    setupFiles: ['jest-canvas-mock']
};
复制代码

其中需要注意的一点是,因为项目中用到了 veui,node_modules 中引用的是源码,未经过 babel 转义。因此需要在 transformIgnorePatterns 中告诉jest,需要对其进行编译。其他引入的第三方库同理。

Vue Test Utils

在编写测试用例之前,我们先来简单了解一下 Vue Test Utils 的 几个 API 以及 Wrapper

mount: 创建一个包含被挂载和渲染的 Vue 组件的 Wrapper
shallowMount: 与mount作用相同,但是不渲染子组件
render: 将一个对象渲染成为一个字符串并返回一个 cheerio 包裹器
createLocalVue:  返回一个 Vue 的类供你添加组件、混入和安装插件而不会污染全局的 Vue 类
复制代码
wrapper: 一个 wrapper 是一个包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法。
wrapper.vm: 可以访问一个实例所有的方法和属性
wrapper.setData() : 同Vue.set()
wrapper.trigger(): 异步触发事件
wrapper.find(): 返回DOM节点或者Vue组件
复制代码

具体的 APIWarpper方法和实例 可以参考这里。

Demo

// test.spec.js
import { shallowMount } from '@vue/test-utils'
import Test from '@/components/Test'

const factory = (values = {}) => {
  return shallowMount(Test, {
    data () {
      return {
        ...values
      }
    }
  })
}

describe('Foo', () => {
  it('renders a welcome message', () => {
    const wrapper = factory()

    expect(wrapper.find('.message').text()).toEqual("Welcome to the Vue.js cookbook")
  })

  it('renders an error when username is less than 7 characters', () => {
    const wrapper = factory({ username: '' })

    expect(wrapper.find('.error').exists()).toBeTruthy()
  })

  it('renders an error when username is whitespace', () => {
    const wrapper = factory({ username: ' '.repeat(7) })

    expect(wrapper.find('.error').exists()).toBeTruthy()
  })

  it('does not render an error when username is 7 characters or more', () => {
    const wrapper = factory({ username: 'Lachlan' })

    expect(wrapper.find('.error').exists()).toBeFalsy()
  })

  it('snapshot test', () => {
    const wrapper = factory()

    expect(wrapper.html()).toMatchSnapshot()
  })
})
复制代码

生成测试覆盖率报告

可以通过配置 jest.config.json增加来生成测试覆盖率报告,生成报告会降低单测的速度,配置中默认是关闭的,需要手动开启。

module.exports = {
  preset: '@vue/cli-plugin-unit-jest',
  // 开启测试报告
  collectCoverage: true,
  // 统计哪里的文件
  collectCoverageFrom: ["**/src/components/**", "!**/node_modules/**"]
}
复制代码

生成的报告在根目录的 coverage 文件夹下,可以通过 package.json 配置命令行打开测试报告或者在控制台查看。

"scripts": {
    "coverage": "open ./coverage/lcov-report/index.html"
  },
复制代码

%Stmts(statement coverage): 语句覆盖率,是否每个语句都执行了?
%Branch(branch coverage): 分支覆盖率,是否每个if代码块都执行了?
%Funcs(branch coverage): 函数覆盖率,是否每个函数都调用了?
%Lines(line coverage): 行覆盖率,是否每一行都执行了?
复制代码

参考文章

  1. jest
  2. Vue Test Utils
  3. Element源码