Vue ㄨ Vitest 测试组件
一、安装及配置
1、vitest
yarn add -D vitest
在 package.json 中添加:
package.json
{
"scripts": {
"test": "vitest"
}
}
Vitest 使用 test、describe、it
等 Jest API 时需要单独 import,可在 test 配置中设置 global: true
,之后无需 import 就能在文件中使用这些 API:
vite.config.ts
/// <reference types="vitest" />
import { defineConfig } from 'vite'
// ...
export default defineConfig({
// ...
test: {
globals: true,
},
})
注意 vite.config.ts 中配置 test 需要将三斜线指令写在文件顶部。
2、vue/test-utils
在编写单元测试时,用一个假组件来替换组件的现有实现,被称为 stub(存根),为了在测试中使用存根,需要用到官方测试工具库 Vue Test Utils(类似 @testing-library/react)的 mount 等方法。安装如下:
yarn add --dev @vue/test-utils@next
3、jsdom
Vitest 的默认测试环境是 Node.js。可以使用 jsdom 或 happy-dom 这种类似浏览器(browser-like)的环境来替代 Node.js。安装如下:
yarn add --dev jsdom
可以在单个测试文件的顶部添加注释来指定测试环境:
index.test.tsx
/**
* @vitest-environment jsdom
*/
// ...
也可以在配置文件中全局配置测试环境(建议):
vite.config.ts
/// <reference types="vitest" />
import { defineConfig } from 'vite'
// ...
export default defineConfig({
// ...
test: {
globals: true,
environment: 'jsdom',
},
})
4、兼容 Vue JSX
当项目中使用 Vue JSX 时,需要进行如下配置以使 .tsx / .jsx
转换为客户端组件:
vite.config.ts
/// <reference types="vitest" />
import { defineConfig } from 'vite'
// ...
export default defineConfig({
// ...
test: {
globals: true,
environment: 'jsdom',
transformMode: {
web: [/\.[jt]sx$/]
}
},
})
二、Jest〡Vitest 测试用例对照
1、生成测试快照
- Vue ㄨ Vitest
- React ㄨ Jest
index.test.tsx
import { mount } from "@vue/test-utils";
import { Button } from '../index';
describe('Button 组件测试', () => {
it('create', () => {
const wrapper = mount({
render() {
return <Button>foo</Button>;
},
});
expect(wrapper.element).toMatchSnapshot();
});
});
index.test.tsx
import { render } from '@testing-library/react';
import Button from '../index';
describe('Button 组件测试', () => {
it('create', () => {
const { asFragment } = render(<Button icon="ThePlus" />);
expect(asFragment()).toMatchSnapshot();
});
});
2、校验查询到的子元素
2-1、匹配指定类名元素是否存在
- Vue ㄨ Vitest
- React ㄨ Jest
index.test.tsx
import { mount } from "@vue/test-utils";
import { Alert } from '../index';
describe('Alert 组件测试', () => {
it('closable', () => {
const wrapper = mount({
render() {
return <Alert message="这是一条消息提示" closable />;
},
});
wrapper.find('.i-alert--close-btn').trigger('click');
expect(wrapper.find('.i-alert').exists()).toBe(false);
})
});
index.test.tsx
import { render, fireEvent } from '@testing-library/react';
import Alert from '../index';
describe('Alert 组件测试', () => {
it('closable', () => {
const { container } = render(<Alert message="这是一条消息提示" closable />);
fireEvent.click(container.querySelector('.i-alert--close-btn'));
expect(container.firstChild).not.toBeInTheDocument();
})
});
2-2、匹配元素的文本是否正确
- Vue ㄨ Vitest
- React ㄨ Jest
index.test.tsx
import { mount } from "@vue/test-utils";
import { Button } from '../index';
describe('Button 组件测试', () => {
it('children', () => {
const wrapper = mount({
render() {
return <Button>foo</Button>;
},
});
expect(wrapper.find('.i-button').text()).toBe('foo');
});
});
index.test.tsx
import { render } from '@testing-library/react';
import Button from '../index';
describe('Button 组件测试', () => {
it('children', () => {
const { queryByText } = render(<Button>foo</Button>);
expect(queryByText('foo')).toBeInTheDocument();
});
});
2-3、匹配指定属性元素是否存在
- Vue ㄨ Vitest
- React ㄨ Jest
index.test.tsx
import { mount } from "@vue/test-utils";
import { Xx } from '../index';
describe('Alert 组件测试', () => {
it('children', () => {
const wrapper = mount({
render() {
return (
<Breadcrumb>
<Breadcrumb.Item>item1</Breadcrumb.Item>
<Breadcrumb.Item maxWidth={80} data-testid='test-item'>item2</Breadcrumb.Item>
<Breadcrumb.Item>item3</Breadcrumb.Item>
</Breadcrumb>
);
},
});
expect(wrapper.find('[data-testid="test-item"]').attributes('style')).toMatch('max-width: 80px;');
});
});
index.test.tsx
import { render } from '@testing-library/react';
import Breadcrumb from '../index';
describe('Breadcrumb 组件测试', () => {
it('maxWidth', () => {
const { getByTestId } = render(
<Breadcrumb>
<Breadcrumb.Item>item1</Breadcrumb.Item>
<Breadcrumb.Item maxWidth={80} data-testid='test-item'>item2</Breadcrumb.Item>
<Breadcrumb.Item>item3</Breadcrumb.Item>
</Breadcrumb>
);
expect(getByTestId('test-item')).toHaveStyle('max-width: 80px;');
});
});
3、根据传入属性校验类名
- Vue ㄨ Vitest
- React ㄨ Jest
index.test.tsx
import { mount } from "@vue/test-utils";
import { Avatar } from '../index';
describe('Avatar 组件测试', () => {
it('shape', () => {
const wrapper = mount({
render() {
return <Avatar shape="round">L</Avatar>;
},
});
expect(wrapper.classes()).toContain('i-avatar__shape-round');
});
});
index.test.tsx
import { render } from '@testing-library/react';
import Avatar from '../index';
describe('Avatar 组件测试', () => {
it('shape', () => {
const { container } = render(<Avatar shape="round">L</Avatar>);
expect(container.firstChild).toHaveClass('i-avatar__shape-round');
})
});
4、根据传入属性校验样式
- Vue ㄨ Vitest
- React ㄨ Jest
注意
vue/test-utils 的 attributes〡getAttribute 会把测试的颜色值转为 rgb 格式。
index.test.tsx
import { mount } from "@vue/test-utils";
import { Avatar } from '../index';
describe('Avatar 组件测试', () => {
it('size', () => {
const wrapper = mount({
render() {
return <Avatar size={24}>L</Avatar>;
},
});
expect(wrapper.element.getAttribute('style')).toMatch('width: 24px;');
// 或
expect(wrapper.find('.i-avatar').attributes('style')).toMatch('width: 24px;');
});
});
index.test.tsx
import { render } from '@testing-library/react';
import Avatar from '../index';
describe('Avatar 组件测试', () => {
it('size', () => {
const { container } = render(<Avatar size={24}>L</Avatar>);
expect(container.firstChild).toHaveStyle('width: 24px;');
})
});
5、测试传入事件是否生效
- Vue ㄨ Vitest
- React ㄨ Jest
index.test.tsx
import { mount } from "@vue/test-utils";
import { vi } from 'vitest';
import { Button } from '../index';
describe('Button 组件测试', () => {
it('onClick', () => {
const clickFn = vi.fn();
const wrapper = mount({
render() {
return <Button onClick={clickFn} />;
},
});
wrapper.findComponent(Button).trigger('click');
expect(clickFn).toBeCalledTimes(1);
// 或
// expect(clickFn).toHaveBeenCalled();
});
});
index.test.tsx
import { render, fireEvent } from '@testing-library/react';
import Button from '../index';
describe('Button 组件测试', () => {
it('onClick', () => {
const clickFn = jest.fn();
const { container } = render(<Button onClick={clickFn} />);
fireEvent.click(container.firstChild);
expect(clickFn).toBeCalledTimes(1);
// 或
// expect(clickFn).toHaveBeenCalled();
});
});
6、测试禁用事件是否生效
- Vue ㄨ Vitest
- React ㄨ Jest
index.test.tsx
import { mount } from "@vue/test-utils";
import { vi } from 'vitest';
import { Button } from '../index';
describe('Button 组件测试', () => {
it('disabled', () => {
const clickFn = vi.fn();
const wrapper = mount({
render() {
return <Button disabled onClick={clickFn} />;
},
});
expect(wrapper.classes()).toContain('i-button-disabled');
wrapper.findComponent(Button).trigger('click');
expect(clickFn).toBeCalledTimes(0);
});
});
index.test.tsx
import { render, fireEvent } from '@testing-library/react';
import Button from '../index';
describe('Button 组件测试', () => {
it('disabled', () => {
const clickFn = jest.fn();
const { container } = render(<Button disabled onClick={clickFn} />);
expect(container.firstChild).toBeDisabled();
fireEvent.click(container.firstChild);
expect(clickFn).toBeCalledTimes(0);
});
});
更多测试用例可参考 iDesign Vue 代码。