为什么要有组件测试?
新建不同的属性的组件,只能肉眼观测
更新代码,要再次进行手动测试
效率太低了
初步安装
js
npm install -D vitest
建一个测试文件:example.test.ts
js
import { describe } from 'node:test'
import { expect, test } from 'vitest'
test('test common matcher', () => {
const name = 'lu'
expect(name).toBe('lu')
expect(2 + 2).toBe(4)
expect(2 + 2).not.toBe(5)
})
test('test to be true or false', () => {
expect(1).toBeTruthy()
expect(0).toBeFalsy()
})
test('test numner', () => {
expect(4).toBeGreaterThan(3)
})
test('test object', () => {
expect({ name: 'lu' }).toEqual({ name: 'lu' })
})
js
npx vitest example
这样就可以显示测试结果:

回调测试、对象方法的调用、第三方库的模拟
js
import axios from 'axios'
export function testFn(number: number, callback: Function) {
if (number > 10) {
callback(number)
}
}
export async function request() {
const { data } = await axios.get('fake.url')
return data
}
js
import { describe } from 'node:test'
import { expect, test, vi, Mocked } from 'vitest'
import { testFn, request } from './utils'
import axios from 'axios'
vi.mock('axios')
const mockAxios = axios as Mocked<typeof axios>
//callback test
describe('function', () => {
test('create a mock funtion', () => {
const callback = vi.fn()
testFn(12, callback)
expect(callback).toHaveBeenCalled()
expect(callback).toHaveBeenCalledWith(12)
})
test('spy on method', () => {
const obj = {
getName: () => 1
}
const spy = vi.spyOn(obj, 'getName')
obj.getName()
expect(spy).toHaveBeenCalled()
obj.getName()
expect(spy).toBeCalledTimes(2)
})
test('mock third party module', async () => {
mockAxios.get.mockImplementation(() => Promise.resolve({ data: 123 }))
const result = await request()
expect(result).toBe(123)
})
})
安装vue-test-utils
js
npm install --save-dev @vue/test-utils
挂载Button组件,写Button.test.ts测试文件:
js
import { describe, test } from 'vitest'
import Button from './Button.vue'
import { mount } from '@vue/test-utils'
describe('Button.vue', () => {
test('basic button', () => {
const wrapper = mount(Button, {
props: {
type: 'primary'
},
slots: {
default: 'button'
}
})
console.log(wrapper.html());
})
})
这里要返回dom,需要配置vitest.confid.ts:
js
/// <reference types="vitest"/>
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import vueJsx from '@vitejs/plugin-vue-jsx'
import vueDevTools from 'vite-plugin-vue-devtools'
// https://vite.dev/config/
export default defineConfig({
plugins: [
vue(),
vueJsx(),
vueDevTools(),
],
test: {
globals: true,
environment: 'jsdom'
}
})
Button组件:
写基本测试
js
import { describe, test, expect } from 'vitest'
import Button from './Button.vue'
import { mount } from '@vue/test-utils'
describe('Button.vue', () => {
test('basic button', () => {
const wrapper = mount(Button, {
props: {
type: 'primary'
},
slots: {
default: 'button'
}
})
console.log(wrapper.html());
expect(wrapper.classes()).toContain('lu-button--primary')
//slot
//get方法
//find方法
expect(wrapper.get('button').text()).toBe('button')
//event
wrapper.get('button').trigger('click')
console.log(wrapper.emitted());
expect(wrapper.emitted()).toHaveProperty('click')
})
test('disabled', () => {
const wrapper = mount(Button, {
props: {
disabled: true
},
slots: {
default: 'disabled'
}
})
expect(wrapper.attributes('disabled')).toBeDefined()
expect(wrapper.find('button').element.disabled).toBeDefined()
expect(wrapper.emitted()).not.toHaveProperty('click')
})
})
测试和图标相关的内容
subs替换功能:
因为图标的实现比较复杂所以使用subs
,在测试 Button
组件时,使用 stubs
替换 FontAwesomeIcon
组件后,测试就可以专注于 Button
组件自身的逻辑和渲染,而不用考虑 FontAwesomeIcon
组件的行为
js
test('icon', () => {
const wrapper = mount(Button, {
props: {
icon: 'arrow-up'
},
slots: {
default: 'icon'
},
global: {
stubs: ['FontAwesomeIcon']
}
})
console.log(wrapper.html());
const iconElement = wrapper.findComponent(FontAwesomeIcon)
expect(iconElement.exists()).toBe(true)
expect(iconElement.attributes('icon')).toBe('arrow-up')
})
test('loading', () => {
const wrapper = mount(Button, {
props: {
loading: true
},
slots: {
default: 'loading'
},
global: {
stubs: ['Icon']
}
})
console.log(wrapper.html());
const iconElement = wrapper.findComponent(Icon)
expect(iconElement.exists()).toBe(true)
expect(iconElement.attributes('icon')).toBe('spinner')
expect(iconElement.attributes('disables')).toBeDefined
})
Collapse组件:
js
import { describe, expect, test } from 'vitest';
import { mount } from '@vue/test-utils';
import Collapse from './Collapse.vue';
import CollapseItem from './CollapseItem.vue';
describe('Collapse.vue', () => {
test('basic collapse', async () => {
const wrapper = mount(() => (
<Collapse modelValue={['a']}>
<CollapseItem name="a" title="title a">
content a
</CollapseItem>
<CollapseItem name="b" title="title b">
content b
</CollapseItem>
<CollapseItem name="c" title="title c" disabled>
content c
</CollapseItem>
</Collapse>
), {
global: {
stubs: ['Icon']
},
attachTo: document.body
});
//是否会渲染出结构
const headers = wrapper.findAll('.lu-collapse-item__header')
const contents = wrapper.findAll('.lu-collapse-item__content')
expect(headers.length).toBe(3)
expect(contents.length).toBe(3)
//文本
const firstHeader = headers[0]
expect(firstHeader.text()).toBe('title a')
//内容
const firstContent = contents[0]
const secondContent = contents[1]
expect(firstContent.isVisible).toBeTruthy()
expect(secondContent.isVisible).toBeTruthy()
//点击
firstHeader.trigger('click');
await wrapper.vm.$nextTick();
expect(firstContent.isVisible()).toBe(false);
});
});
使用 await wrapper.vm.$nextTick()
后,代码会暂停执行,直到 DOM 更新完成。之后再执行 expect(firstContent.isVisible()).toBe(false)
这一断言,就能确保此时获取到的 firstContent
的显示状态是最新的,这样的断言结果才是准确可靠的。