一、关于测试
1、测试分类
- 单元测试:unit test
- 集成测试
- 端对端测试:e2e
2、通用测试框架:
- jest
- vitest
只能做js、ts测试。运行环境是node,没有dom和window
如果需要测试dom,需要安装jsdom
,用于在node环境中模拟dom
二、vitest
1、基本使用
- 安装
npm i vitest -D
- 配置启动命令
"scripts": {
"test": "vitest"
}
- 新建以
.spec.ts
或.test.ts
结尾的文件
import { expect, test } from 'vitest'
test('测试通用匹配', () => {
const name = 'button'
// expect 用于创建断言,toBe 可用于断言基元是否相等或对象共享相同的引用。
expect(name).toBe('button')
expect(2 + 2).toBe(4)
expect(2 + 2).not.toBe(5)
})
2、测试不同类型
2.1 测试boolean值
test('测试true或false', () => {
expect(true).toBeTruthy() // 断言值在转换为布尔值时为 true
expect(false).toBeFalsy() // 断言值在转换为布尔值时为 false
})
2.2 测试null或undefined
test('测试null或undefined', () => {
expect(null).toBeNull()
expect(undefined).toBeUndefined()
})
2.3 测试number大小
test('测试number', () => {
expect(4).toBeGreaterThan(3) // 4大于3
expect(4).toBeGreaterThanOrEqual(4) // 4大于等于4
expect(4).toBeLessThan(5) // 4小于5
})
- 测试对象
⚠️注意:测试对象时需要注意,使用toBe会测试不通过,因为对象是引用类型
使用
toEqual
:用于比较两个值是否相等,支持深层对象和数组的比较。
test('测试对象', () => {
// 正确写法
// toEqual:用于比较两个值是否相等,支持深层对象和数组的比较。
expect({ name: 'button' }).toEqual({ name: 'button' })
// 错误写法
// expect({ name: 'button' }).toBe({ name: 'button' })
})
2.4 测试回调函数
使用vi.fn
:创建函数的监视程序,返回值callback(一个在需要地方调用的函数)
toHaveBeenCalled
:已经被调用toHaveBeenCalledWith
:被调用时传入的参数
import { expect, test, describe, vi } from 'vitest'
import { testFn } from './utils'
// describe:将测试用例分组
describe('test function', () => {
test('创建模拟函数', () => {
// vi.fn():创建函数的监视程序
const callback = vi.fn() // 监听callback函数
testFn(11, callback)
expect(callback).toHaveBeenCalled() // toHaveBeenCalled:已经被调用
expect(callback).toHaveBeenCalledWith(11) // toHaveBeenCalledWith:被调用时传入的参数
})
})
// utils.ts
import axios from 'axios'
export function testFn(number: number, callback: Function) {
if (number > 10) {
callback(number)
}
}
2.5 监控对象上的方法
使用vi.spyOn
,创建与 vi.fn()
类似的对象的方法或 getter/setter 的监听(spy) 。它会返回一个 mock 函数 。
toHaveReturnedWith
:返回值toHaveBeenCalledTimes
:被调用次数
import { expect, test, describe, vi } from 'vitest'
describe('test obj.fn', () => {
test('监控对象上的方法', () => {
const obj = {
getName: () => 1
}
// 创建监听obj.getName方法
const spy = vi.spyOn(obj, 'getName')
obj.getName()
expect(spy).toHaveBeenCalled()
expect(spy).toHaveReturnedWith(1) // toHaveReturnedWith:返回值
obj.getName()
expect(spy).toHaveBeenCalledTimes(2) // toHaveBeenCalledTimes:被调用次数
})
})
2.6 模拟第三方模块(以axios为例)
- 需要在测试的异步方法中使用axios
// utils.ts
import axios from 'axios'
export async function testAxios() {
const { data } = await axios.get('https://xxx.com')
return data
}
- 在测试文件中导入axios,将axios替换一个带ts类型的模拟axios
mockImplementation
:模拟axios成功的结果,手动一个promise对象mockResolvedValue
:模拟axios成功的结果,返回一个promise对象
import { expect, test, describe, vi, Mocked } from 'vitest'
import { testAxios } from './utils'
import axios from 'axios'
vi.mock('axios') // 模拟axios模块,后续的axios调用其实是替换了模拟的axios模块
const mockAxios = axios as Mocked<typeof axios> // 将axios替换成有ts类型的axios模块
describe('test axios', () => {
test('模拟第三方模块', async () => {
// 写法1: mockImplementation:模拟实现,返回一个promise对象
mockAxios.get.mockImplementation(() => Promise.resolve({ data: 123 }))
// 写法2: mockResolvedValue:模拟实现,返回一个promise对象
mockAxios.get.mockResolvedValue({ data: 123 })
const result = await testAxios() // 相当于调用testAxios返回的data是mockAxios模拟返回的数据
expect(result).toBe(123)
})
})
代码讲解:
1、
import axios from 'axios'
vi.mock('axios') // 模拟axios模块,后续的axios调用其实是替换了模拟的axios模块
const mockAxios = axios as Mocked<typeof axios> // 将axios替换成有ts类型的axios模块
这里是导入axios,用vitest的axios去替换真实的axios(类似“狸猫换太子”)。mockAxios
是将vitest的axios重新赋值,变为一个带有ts类型的axios
2、
mockAxios.get.mockImplementation(() => Promise.resolve({ data: 123 }))
mockAxios.get
:就是axios.get()
方法,发送get请求
3、覆盖率测试
- 安装
@vitest/coverage-v8
(覆盖率测试安装包)
npm i @vitest/coverage-v8 -D
- 添加脚本
"scripts": {
"coverage": "vitest run --coverage"
}
- 配置
vite.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
coverage: {
include: ['src/components/**'], // 哪些文件需要做覆盖率测试
exclude: ['src/**/schema.ts'] // 排除哪些文件
}
}
})
三、vue、react框架测试工具
- vue-test-utils:测试vue(https://test-utils.vuejs.org/zh/guide/)
- @testing-library:测试vue、react(https://cn.vitest.dev/guide/browser/examples.html#examples)
1、vue-test-utils
vitest只能做js、ts测试,vue-test-utils
能帮助解决测试vue组件
1.1 基本使用(包括配置vite.config.ts)
- 安装
npm i @vue/test-utils -D
- 使用
调用mount
方法,渲染组件。参数1:组件实例,参数2:值
import { describe, test, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from './src/button.vue'
describe('测试 Button.vue', () => {
test('render', () => {
// 渲染组件
const wrapper = mount(Button, {
props: {
type: 'primary'
},
slots: {
default: '按钮' // 默认插槽
}
})
console.log(wrapper.html());
})
})
但是,这里运行命令会报错:
FAIL button/button.test.ts > 测试 Button.vue > render
ReferenceError: document is not defined
❯ Proxy.mount ../../node_modules/.pnpm/@vue+test-utils@2.4.6/node_modules/@vue/test-utils/dist/vue-test-utils.cjs.js:8401:14
❯ button/button.test.ts:8:21
6| test('render', () => {
7| // 渲染组件
8| const wrapper = mount(Button, {
| ^
9| props: {
10| type: 'primary'
ReferenceError: document is not defined
意思是:没有dom环境,vitest默认环境是node,需要添加dom环境,安装jsdom
npm i jsdom -D
同时,配置vite.config.ts
/// <reference types="vitest" />
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
export default defineConfig({
plugins: [
vue(),
vueJsx() as any,
],
test: {
/**
* 在 Vitest 中,globals 配置项用于控制是否启用全局 API。
* - globals: false(默认情况):
行为:Vitest 不会自动提供 Jest 的全局 API(如 describe、it、expect 等)。
使用方式:你需要显式地导入这些 API。
- globals: true
行为:Vitest 会自动提供 Jest 的全局 API,你不需要显式导入这些 API。
使用方式:可以直接使用这些全局 API。
*/
globals: true, // 启用全局模式
environment: 'jsdom', // 模拟浏览器环境
}
})
vitest和vite可以共用同一个配置文件
vite.config.ts
,vitest也可以新建自己的vitest.config.ts
1.2 测试Button组件内容、类名
get
:获取元素,找不到就报错find
:获取元素,找不到就返回 null
import { describe, test, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from './src/button.vue'
import { Accessibility } from '@vicons/ionicons5'
describe('测试 Button.vue', () => {
test('basic button', () => {
// 渲染组件
const wrapper = mount(Button, {
props: {
type: 'primary'
},
slots: {
default: '按钮' // 默认插槽
}
})
expect(wrapper.classes()).toContain('gy-button--primary') // 测试类名是否存在
// slot内容:可以用 get 和 find 方法
/**
* get 和 find 区别:
* get 是获取元素,找不到就报错,find 是获取元素,找不到就返回 null
*/
expect(wrapper.get('.gy-button').text()).toBe('按钮') // 测试文本内容(slot)
expect(wrapper.find('.gy-button').exists()).toBe(true) // exists:判断元素是否存在
})
})
1.3 测试Button组件事件
trigger('click')
:触发事件emitted()
:返回一个对象toHaveProperty(key)
:测试对象上是否有某个属性
import { describe, test, expect } from 'vitest'
import { mount } from '@vue/test-utils'
import Button from './src/button.vue'
import Icon from '../icon/src/icon.vue'
import { Accessibility } from '@vicons/ionicons5'
describe('测试 Button.vue', () => {
test('basic button', () => {
// 渲染组件
const wrapper = mount(Button, {
props: {
type: 'primary'
},
slots: {
default: '按钮' // 默认插槽
}
})
// 测试事件
// wrapper.trigger('click')
wrapper.get('button').trigger('click')
console.log(wrapper.emitted()); // 返回一个对象:{ click: [ [ [MouseEvent] ] ] }
expect(wrapper.emitted()).toHaveProperty('click'); // 测试对象上是否有某个属性
})
})
1.4 测试Button组件的disabled属性
方法:
wrapper.attributes('disabled')
:通过attributes
获取属性wrapper.find('button').element.disabled
:获取到真实的DOM元素,再获取属性toBeDefined()
:测试属性是否存在
要点:
- 看能否获取到组件的
disabled
属性 - 有
disabled
后,点击事件不会被触发
test('test button disabled', () => {
const wrapper = mount(Button, {
props: {
disabled: true
},
slots: {
default: '按钮'
}
})
// 方法1:attributes获取属性
expect(wrapper.attributes('disabled')).toBeDefined() // 测试属性是否存在
// 方法2:获取到真实的DOM元素
expect(wrapper.find('button').element.disabled).toBeDefined()
// 判断点击后是否会发送事件(disabled不会发送事件)
wrapper.get('button').trigger('click')
console.log(wrapper.emitted()); // {}
expect(wrapper.emitted()).not.toHaveProperty('click') // 测试对象上是否有某个属性
})
1.5 测试Button组件嵌套的Icon组件
**不模拟Icon组件(不忽略Icon组件测试):**也就是在slots.icon
插槽中填入icon
import Icon from '../icon/src/icon.vue'
import { Accessibility } from '@vicons/ionicons5' // 第三方图标
test('test button icon测试1:不忽略icon组件', () => {
const icon = mount(Icon, {
slots: {
default: Accessibility
},
})
const wrapper = mount(Button, {
props: {
iconPlacement: 'left'
},
slots: {
default: '按钮',
icon: icon
}
})
console.log(wrapper.html());
// icon组件是否有图标
const iconElement = icon.findComponent(Accessibility) // 获取到组件实例
expect(iconElement.exists()).toBeTruthy()
// button组件是否包含icon
const buttonElement = icon.findComponent(Icon)
expect(buttonElement.exists()).toBeTruthy()
})
模拟Icon组件(忽略Icon组件测试):
在icon组件中,如果要引入图标,测试起来会慢一些,使用stubs
将该组件忽略掉,就不会对Icon组件进行测试
test('test button icon测试2:忽略icon组件', () => {
const wrapper = mount(Button, {
props: {
loading: true,
iconPlacement: 'left'
},
slots: {
default: '按钮',
icon: 'icon'
},
global: {
stubs: ['Icon'] // 忽略icon组件(当icon是依赖第三方的,可以使用stubs忽略)
}
})
console.log(wrapper.html());
// button组件是否包含icon
const buttonElement = wrapper.findComponent(Icon)
expect(buttonElement.exists()).toBeTruthy()
})
1.6 测试tsx
下面是一个tsx写的组件:
在tsx中使用插槽,需要用useSlots()
这个hook
// VNode.tsx
import { defineComponent, useSlots } from 'vue'
export default defineComponent({
props: {
title: {
type: String,
default: 'world'
}
},
setup(props) {
const slots = useSlots()
return () => (
<div>
<h1>hello + {props.title}</h1>
{slots.default?.() as any}
</div>
)
}
})
测试代码:
有两种写法:
- 使用
h
渲染函数tsx
写法
1、使用h
渲染函数写法:
h
函数使用方法:
function h(
type: string | Component,
props?: object | null,
children?: Children | Slot | Slots
): VNode
h(<div>hello</div>, props, 内容)
import { describe, expect, test } from 'vitest'
import { h } from 'vue'
import { mount } from '@vue/test-utils'
import VNode from './src/VNode'
import Button from './src/button.vue'
test('测试tsx 写法1', () => {
const wrapper = mount(VNode, {
props: {
title: '标题'
},
slots: {
default: () => h(Button, { type: 'primary' }, { default: () => '按钮' }) // 渲染button组件
}
})
console.log(wrapper.html())
})
2、使用tsx
写法:
test('测试tsx 写法2', () => {
const wrapper = mount(() => (
<VNode title={'标题'}>
<Button type="primary">按钮</Button>
</VNode>
), {
props: {
title: '标题'
},
slots: {
default: () => h(Button, { type: 'primary' }, { default: () => '按钮' }) // 渲染button组件
}
})
console.log(wrapper.html())
})
2、@testing-library
@testing-library
有可以测试vue和react,具体用法看官网:
- react:https://testing-library.com/docs/react-testing-library/intro/
- vue:https://testing-library.com/docs/vue-testing-library/intro
⚠️注意:下面内容以react为例
2.1 安装依赖
"vitest": "^2.1.8",
"@testing-library/react": "^16.1.0", // 组件测试,用于渲染组件
"@testing-library/dom": "^10.4.0", // 组件测试,用于测试 dom 元素
"@testing-library/jest-dom": "^6.6.3", // 扩展测试函数
"jsdom": "^25.0.1", // 模拟 dom 环境
2.2 配置vite.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
globals: true, // 全局变量
environment: 'jsdom' // 当前环境
}
})
2.3 测试Button组件
import React from 'react'
import { describe, test, expect } from 'vitest'
import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom' // 扩展测试函数,例如:toHaveClass
import Button from '.'
describe('Button Component', () => {
test('should render correctly', async () => {
// 渲染组件
const dom = await render(<Button />)
// 获取组件:screen
const Btn = screen.getByText('Click me')
// 是否符合预期
expect(dom).toBeTruthy()
expect(Btn).toHaveClass('btn')
})
})
2.4 测试Button组件事件
模拟函数
:const handleClick = vi.fn()
import React from 'react'
import { describe, test, expect, vi } from 'vitest'
import { render, screen } from '@testing-library/react'
import '@testing-library/jest-dom' // 扩展测试函数,例如:toHaveClass
import Button from '.'
describe('Button Component', () => {
test('should render correctly', async () => {
// mock 函数
const handleClick = vi.fn()
// 渲染组件
const dom = await render(<Button onClick={handleClick} />)
// 获取组件:screen
const Btn = await screen.getByText('Click me')
// 点击
Btn.click()
// 是否符合预期
expect(dom).toBeTruthy()
expect(Btn).toHaveClass('btn')
expect(handleClick).toBeCalled()
})
})