4-打造属于你的组件库-【单元测试】

184 阅读4分钟

单元测试

测试驱动开发(TDD): 在开发功能代码前,先编写好单元测试用例代码,通过测试用例来规范约束开发者编写出质量更高,bug 更少的代码,侧重点偏向开发

行为驱动开发(BDD): 侧重设计,在设计测试用例的时候,需要了解到使用的场景,分析出具体的场景案例,然后再将这些转换成测试代码。。就尼玛离谱

目前比较推荐的是 Jest自带断言库代码覆盖率检测 。还有一个叫 Mocha 的,但需要配合 Chai 断言库使用。

还有一个 Vite 官配的 Vitest

一. Jest

Jest 中文文档

安装

# 安装 jest
yarn add jest -D

# 由于 nodeJs 不支持部分ES6写法,所以需要 babel ,同时根目录添加 .babelrc
yarn add @babel/core @babel/preset-env -D

# 添加 @babel/preset-typescript 支持 TS
yarn add @babel/preset-typescript -D

# 添加 @types/jest 使编辑器不对 test.ts 文件的 jest 进行报错
yarn add @types/jest -D

# 添加 babel插件支持async await
yarn add @babel/plugin-transform-runtime -D

【注】关于类型,由于 BabelTypescript 的支持是纯编译形式(无类型校验),因此 Jest 在运行测试时不会对其进行类型检测,如果需要类型检测,可以改用 ts-jest

jest 运行时内部先执行(jest-babel), 检测是否安装 babel-core, 然后取 .babelrc 中的配置, 运行测试之前结合 babel 先把测试用例代码转换一遍然后再进行测试

// .babelrc 配置
{
  "presets": [
    "@babel/preset-env",
    "@babel/preset-typescript"
  ],
  "plugins": ["@babel/plugin-transform-runtime"]
}

单元测试覆盖率

单元测试覆盖率是一种软件测试的度量指标,指在所有功能代码中,完成了单元测试的代码所占的比例。有很多自动化测试框架工具可以提供这一统计数据,其中最基础的计算方式为:

单元测试覆盖率 = 被测代码行数 / 参测代码总行数 * 100%

如何生成?

添加 jest.config.js 文件

module.exports = {
  // 是否显示覆盖率报告
  collectCoverage: true,
  // 告诉 jest 哪些文件需要经过单元测试测试
  collectCoverageFrom: ['./*.ts']
}

参数解读:

参数名含义说明
Stmts语句覆盖率是否每个语句都执行了
Branch分支覆盖率是否每个 if 代码块都执行了
Funcs函数覆盖率是否每个函数都调用了
Lines行覆盖率是否每行都执行了

二. Vitest

一个 Vite 原生的极速单元测试框架。因为同样使用了 Esbuild 对文件进行 transform 所以速度非常之快

为什么要使用 Vitest?

  1. 它的 API 几乎与 Jest 相同,且开箱即支持 Typescript / Jsx / Esm, 模拟 DOM
  2. 能够模拟 DOM
  3. 生成测试覆盖率
  4. 可以对 Vue/React 组件进行测试
  5. 与 Vite 的配置通用,watch 模式下极快的反应(相当于测试中 HMR(热更新))

安装

yarn add vitest -D

其他跟 Jest 差不多

如何进行 DOM 测试?

首先 Vitest 支持 使用 jsdomhappy-dom 模拟 DOM

其次,还需要一个 存根(stub) 来作为假组件代替真实组件。 Vue 官方提供了 Vue Test Utils 测试工具库,里面的 mount 方法可以帮你挂载。

# 挂载DOM
yarn add @vue/test-utils@next -D
# 模拟DOM
yarn add happy-dom -D

假组件内存在第三方组件

// 由于使用了第三方框架,需要在这里全局引入一下
import ElementUI from 'element-plus'
config.global.plugins = [ElementUI]

案例

/**
 * @vitest-environment happy-dom
 */

import { mount } from '@vue/test-utils'
import notification from '../components/notification.vue'
import { describe, expect, test } from 'vitest'

describe('notification.vue', () => {
  test('renders the correct style for error', () => {
    const type = 'error'
    const wrapper = mount(notification, {
      props: { type }
    })
    expect(wrapper.classes()).toEqual(expect.arrayContaining(['notification--error']))
  })

  test('renders the correct style for success', () => {
    const type = 'success'
    const wrapper = mount(notification, {
      props: { type }
    })
    expect(wrapper.classes()).toEqual(expect.arrayContaining(['notification--success']))
  })

  test('renders the correct style for info', () => {
    const type = 'info'
    const wrapper = mount(notification, {
      props: { type }
    })
    expect(wrapper.classes()).toEqual(expect.arrayContaining(['notification--info']))
  })

  test('slides down when message is not empty', () => {
    const message = 'success'
    const wrapper = mount(notification, {
      props: { message }
    })
    expect(wrapper.classes()).toEqual(expect.arrayContaining(['notification--slide']))
  })

  test('slides up when message is empty', () => {
    const message = ''
    const wrapper = mount(notification, {
      props: { message }
    })
    expect(wrapper.classes('notification--slide')).toBe(false)
  })

  test('emits event when close button is clicked', async () => {
    const wrapper = mount(notification, {
      data() {
        return {
          clicked: false
        }
      }
    })
    const closeButton = wrapper.find('button')
    await closeButton.trigger('click')
    expect(wrapper.emitted()).toHaveProperty('clear-notificatioon')
  })

  test('renders message when message is not empty', () => {
    const message = 'Something happened, try again'
    const wrapper = mount(notification, {
      props: { message }
    })
    expect(wrapper.find('p').text()).toBe(message)
  })
})