测试所用代码
- 使用了全局组件
a-button
a-menu
- ... ...
- 使用了外部的模块
useStore
useRouter
message
<template>
<!-- 登录按钮 -->
<a-button type="primary"
v-if="!user.isLogin"
@click="login"
class="user-profile-component">
登录
</a-button>
<!-- 登出下拉 -->
<div v-else>
<a-dropdown-button class="user-profile-component">
<!-- 显示用户名 -->
<router-link to="/setting">{{ user.username }}</router-link>
<template v-slot:overlay>
<a-menu class="user-profile-dropdown">
<a-menu-item key="0"
@click="logout">登出</a-menu-item>
</a-menu>
</template>
</a-dropdown-button>
</div>
</template>
<script lang="ts" setup>
import { defineProps } from 'vue'
import { useStore } from 'vuex'
import { useRouter } from 'vue-router'
import { message } from 'ant-design-vue'
import { UserProps } from '../store/user'
interface Props {
user: UserProps;
}
const props = defineProps<Props>()
const store = useStore()
const router = useRouter()
// 登录
const login = () => {
store.commit("login")
message.success("登录成功", 2)
}
// 登出
const logout = () => {
store.commit('logout')
message.success('退出登录成功,2秒后跳转到首页', 2)
setTimeout(() => {
router.push('/')
}, 2000)
}
</script>
mock 全局组件
代码语言:javascript复制import type { VueWrapper } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import UserProfile from '@/components/UserProfile.vue';
// 模拟第三方库 ant-design-vue
jest.mock('ant-design-vue');
// 模拟外部模块 vuex
jest.mock('vuex');
// 模拟外部模块 vue-router
jest.mock('vue-router');
// 定义 wrapper
let wrapper: VueWrapper<any>;
describe('UserProfile.vue', () => {
beforeEach(() => {
// 获取组件
wrapper = mount(UserProfile, {
// 传入到组件内部的属性
props: { user: { isLogin: false } },
});
});
// 测试没有登录的时候
it('should render login button when login is false', async () => {
console.log(wrapper.html());
});
// 测试已经登录的时候
it('should render username when login is true', async () => {
//
});
afterEach(() => {
//
});
});
此时会出现类似于 Failed to resolve component: a-button
的报错
此时需要在 mount
方法的第二个参数中定义全局组件
import type { VueWrapper } from '@vue/test-utils';
import { mount } from '@vue/test-utils';
import UserProfile from '@/components/UserProfile.vue';
// 模拟第三方库 ant-design-vue
jest.mock('ant-design-vue');
// 模拟外部模块 vuex
jest.mock('vuex');
// 模拟外部模块 vue-router
jest.mock('vue-router');
// 模拟组件
const mockComponent = {
template: '<div><slot></slot></div>',
};
// 模拟剧名插槽的组件
const mockComponent2 = {
template: '<div><slot></slot><slot name="overlay"></slot></div>',
};
// 全局组件列表
const globalComponents = {
'a-button': mockComponent,
'a-dropdown-button': mockComponent2,
'router-link': mockComponent,
'a-menu': mockComponent,
'a-menu-item': mockComponent,
};
// 定义 wrapper
let wrapper: VueWrapper<any>;
describe('UserProfile.vue', () => {
beforeEach(() => {
// 获取组件
wrapper = mount(UserProfile, {
// 传入到组件内部的属性
props: { user: { isLogin: false } },
// 定义全局组件
global: {
components: globalComponents,
// 如果是在文件中注册的组件,需要使用 stubs
stubs:globalComponents
},
});
});
// 测试没有登录的时候
it('should render login button when login is false', async () => {
console.log(wrapper.html());
// 断言 div 上的文字是 登录
expect(wrapper.get('div').text()).toBe('登录');
});
// 测试已经登录的时候
it('should render username when login is true', async () => {
// 修改传入组件内部的属性
await wrapper.setProps({
user: { isLogin: true, username: 'warbler' },
});
console.log(wrapper.html());
// 断言 .user-profile-component 的内容是 warbler
expect(wrapper.get('.user-profile-component').html()).toContain('warbler');
// 断言 .user-profile-dropdown 存在
expect(wrapper.find('.user-profile-dropdown').exists()).toBeTruthy();
});
afterEach(() => {
//
});
});
这样就完成了。
模拟第三方库
ant-design-vue message
ant-design-vue
组件库的 message
如何 mock
// 先引入真实的 message 方法
import { message } from 'ant-design-vue';
// 模拟第三方库 ant-design-vue,第二个参数模拟方法
jest.mock('ant-design-vue', () => ({
message: {
success: jest.fn(),
},
}));
// 定义 wrapper
let wrapper: VueWrapper<any>;
describe('UserProfile.vue', () => {
// 测试没有登录的时候
it('should render login button when login is false', async () => {
console.log(wrapper.html());
// 断言 div 上的文字是 登录
expect(wrapper.get('div').text()).toBe('登录');
// 触发点击事件
await wrapper.get('div').trigger('click');
// 断言 message.success 被触发了
expect(message.success).toHaveBeenCalled();
});
});
vuex
vuex
可以使用模拟的方式来完成,但是更直接的是直接使用真实的 store
。
// 引入真实的 vuex store
import store from '@/store/index';
// 使用 provide 挂载真实的 vuex store ,就无需 mock了
// 定义 wrapper
let wrapper: VueWrapper<any>;
describe('UserProfile.vue', () => {
beforeEach(() => {
// 获取组件
wrapper = mount(UserProfile, {
// 传入到组件内部的属性
props: { user: { isLogin: false } },
global: {
// 定义全局组件
components: globalComponents,
// 注入真实的 vuex store
provide: {
store,
},
},
});
});
// 测试没有登录的时候
it('should render login button when login is false', async () => {
console.log(wrapper.html());
// 断言 div 上的文字是 登录
expect(wrapper.get('div').text()).toBe('登录');
// 触发点击事件
await wrapper.get('div').trigger('click');
// 断言 message.success 被触发了
expect(message.success).toHaveBeenCalled();
// 断言 vuex store 发生了变化
expect(store.state.user.userName).toBe(warbler);
});
});
vue-router
代码语言:javascript复制// 用来模拟 vue-router 的 push 方法
const mockRoutes: string[] = [];
// 模拟外部模块 vue-router
jest.mock('vue-router', () => ({
useRouter: () => ({
push: (url: string) => mockRoutes.push(url),
}),
}));
// 定义 wrapper
let wrapper: VueWrapper<any>;
describe('UserProfile.vue', () => {
beforeAll(() => {
// 拦截所有的 timer
jest.useFakeTimers();
// 获取组件
wrapper = mount(UserProfile, {
// 传入到组件内部的属性
props: { user: { isLogin: false } },
// 定义全局组件
global: {
components: globalComponents,
provide: {
store,
},
},
});
});
// 测试登出的时候
it('should call logout and show message,call router.push after timeout', async () => {
// 触发点击事件
await wrapper.get('.user-profile-dropdown div').trigger('click');
// 断言 修改了 store 的值
expect(store.state.user.isLogin).toBeFalsy();
// 断言 message 被触发了一次
expect(message.success).toHaveBeenCalledTimes(1);
// 消除所有 timer
jest.runAllTimers();
// 断言 触发了 vue-router 的 push 方法
expect(mockRoutes).toEqual(['/']);
});
afterEach(() => {
// 重置mock
(message as jest.Mocked<typeof message>).success.mockReset();
});
});
单独测试 Vuex store
Vuex store
天生就是脱离组件独立开来的。它是一个独立的数据结构,使用特定的方法,更新其中的状态。
测试 Vuex store
非常有必要,当交互变的复杂了以后,可以脱离界面对数据的改动做测试,最大限度的保障功能的正常运行。
测试过程
- 检查初始
state
是否正常 - 触发
mutations
或者actions
,对于每个mutations
可以写一个case
- 检查修改后的
state
是否正常 - 测试
getters
测试代码
代码语言:javascript复制import store from '@/store/index';
import { testData } from '@/store/template';
import { testComponents, ComponentData } from '@/store/editor';
import { TextComponentProps } from '../../src/defaultProps';
import { clone, last } from 'lodash-es';
const cloneComponent = clone(testComponents);
// 测试 vuex store
describe('test vuex store', () => {
// 测试 vuex 有三个模块
it('should have three modules', () => {
expect(store.state).toHaveProperty('user');
expect(store.state).toHaveProperty('template');
expect(store.state).toHaveProperty('editor');
});
// 测试 user 模块
describe('test user module', () => {
// 测试 login mutation
it('test login mutation', () => {
// 执行 login mutation
store.commit('login');
// 断言 isLogin 为 true
expect(store.state.user.isLogin).toBeTruthy();
});
// 测试 logout mutation
it('test logout mutation', () => {
// 执行 logout mutation
store.commit('logout');
// 断言 isLogin 为 false
expect(store.state.user.isLogin).toBeFalsy();
});
});
// 测试 template 模块
describe('test template module', () => {
// 测试默认模板
it('should have default templates', () => {
// 断言 初始数据是否正确
expect(store.state.template.data).toHaveLength(testData.length);
});
// 测试 getters
it('should get the current template by Id', () => {
// 获取 getters
const selectTemplate = store.getters.getTemplateById(1);
// 断言 getters 是否正确
expect(selectTemplate.title).toBe('test title 1');
});
});
// 测试 editor 模块
describe('test editor module', () => {
// 测试初始数据
it('should have default components', () => {
expect(store.state.editor.components).toHaveLength(testComponents.length);
});
//
it('should get current component when set active one component', () => {
// 执行 setActive commit
store.commit('setActive', testComponents[0].id);
// 断言 currentElement 为 新增的 currentElement
expect(store.state.editor.currentElement).toBe(testComponents[0].id);
// 获取 getters 当前 currentElement 对应的组件对象
const currentElement = store.getters.getCurrentElement;
// 断言 当前id 等于 新增的组件 id
expect(currentElement.id).toBe(testComponents[0].id);
});
// 测试 addComponent commit
it('add component should works fine', () => {
const payload: Partial<TextComponentProps> = {
text: 'text1',
};
// 执行 addComponent commit
store.commit('addComponent', payload);
// 断言 数据的长度 1
expect(store.state.editor.components).toHaveLength(
cloneComponent.length 1,
);
// 取到数据最后一项
const lastItem = last(store.state.editor.components);
// 判断文本是否正确
expect(lastItem?.props.text).toBe('text1');
});
// 测试 update commit
it('update component should works fine', () => {
const newProps = {
key: 'text',
value: 'update',
};
// 执行 update commit
store.commit('updateComponent', newProps);
// 获取 getters 当前 currentElement 对应的组件对象
const currentElement = store.getters.getCurrentElement;
// 断言 文本是否正确完成修改
expect(currentElement.props.text).toBe('update');
});
});
});