单元测试
测试驱动开发(TDD): 在开发功能代码前,先编写好单元测试用例代码,通过测试用例来规范约束开发者编写出质量更高,bug 更少的代码,侧重点偏向开发
行为驱动开发(BDD): 侧重设计,在设计测试用例的时候,需要了解到使用的场景,分析出具体的场景案例,然后再将这些转换成测试代码。。就尼玛离谱
目前比较推荐的是 Jest , 自带断言库 和 代码覆盖率检测 。还有一个叫 Mocha 的,但需要配合 Chai 断言库使用。
还有一个 Vite 官配的 Vitest
一. 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
【注】关于类型,由于 Babel 对 Typescript 的支持是纯编译形式(无类型校验),因此 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?
- 它的 API 几乎与
Jest相同,且开箱即支持Typescript / Jsx / Esm, 模拟 DOM - 能够模拟 DOM
- 生成测试覆盖率
- 可以对 Vue/React 组件进行测试
- 与 Vite 的配置通用,watch 模式下极快的反应(相当于测试中 HMR(热更新))
安装
yarn add vitest -D
其他跟 Jest 差不多
如何进行 DOM 测试?
首先 Vitest 支持 使用 jsdom 或 happy-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)
})
})