首页
学习
活动
专区
圈层
工具
发布
50 篇文章
1
【架构师(第一篇)】整体需求分析和架构设计
2
【架构师(第二篇)】脚手架架构设计和框架搭建
3
【架构师(第三篇)】脚手架开发之掌握Lerna操作流程
4
【架构师(第四篇)】脚手架开发之Lerna源码分析
5
【架构师(第五篇)】脚手架之import-local执行流程及简历设计
6
【架构师(第六篇)】脚手架之需求分析和架构设计
7
【架构师(第七篇)】脚手架之准备阶段编写
8
【架构师(第八篇)】脚手架之 commander 框架使用方法
9
【架构师(第九篇)】如何让 Node 环境支持 ES Module
10
【架构师(第十篇)】脚手架之注册命令及架构优化
11
【架构师(第十一篇)】脚手架之命令注册和执行过程开发
12
【架构师(第十二篇)】脚手架之命令行交互工具 inquirer.js 使用方法
13
【架构师(第十三篇)】脚手架之创建项目准备阶段开发
14
【架构师(第十四篇)】脚手架之 egg.js 和 mongodb 的使用
15
【架构师(第十五篇)】脚手架之创建项目模板开发
16
【架构师(第十六篇)】脚手架之创建项目模板的下载与更新
17
【架构师(第十七篇)】脚手架之 ejs 和 glob 的使用
18
【架构师(第十八篇)】脚手架之项目模板的安装
19
【架构师(第十九篇)】脚手架之组件库模板开发
20
【架构师(第二十篇)】脚手架之自定义模板及第一阶段总结
21
【架构师(第二十一篇)】编辑器开发之需求分析和架构设计
22
【架构师(第二十二篇)】编辑器开发之项目整体搭建
23
【架构师(第二十三篇)】编辑器开发之画布区域组件的渲染
24
【架构师(第二十四篇)】编辑器开发之添加模版到画布
25
【架构师(第二十五篇)】编辑器开发之属性编辑区域表单渲染
26
【架构师(第二十六篇)】编辑器开发之属性编辑同步渲染
27
【架构师(第二十七篇)】前端单元测试框架 Jest 基础知识入门
28
【架构师(第二十八篇)】 测试工具 Vue-Test-Utils 基础语法
29
【架构师(第二十九篇)】Vue-Test-Utils 触发事件和异步请求
30
【架构师(第三十篇)】Vue-Test-Utils 全局组件和第三方库 vuex | vue-router
31
【架构师(第三十一篇)】前端测试之 TDD 的开发方式
32
【架构师(第三十二篇)】 通用上传组件开发及测试用例
33
【架构师(第三十三篇)】 Vue 中的实例及本地图片预览
34
【架构师(第三十四篇)】 业务组件库开发之 vue3 的插件系统
35
【架构师(第三十五篇)】 业务组件库开发之使用 Rollup 进行打包
36
【架构师(第三十六篇)】 业务组件库开发之发布到 NPM
37
【架构师(第三十七篇)】 服务端开发之后端框架与数据库技术选型
38
【架构师(第三十八篇)】 服务端开发之本地安装最新版 MySQL 数据库
39
【架构师(第三十九篇)】 服务端开发之连接 MySQL 数据库
40
【架构师(第四十篇)】 服务端开发之连接 Mongodb 数据库
41
【架构师(第四十一篇)】 服务端开发之安装并连接 Redis数据库
42
【架构师(第四十二篇)】 服务端开发之常用的登录鉴权方式
43
【架构师(第四十三篇)】 服务端开发之单元测试和接口测试
44
【架构师(第四十四篇)】 服务端开发之 pm2 和 nginx 介绍
45
【架构师(第四十五篇)】 服务端开发之认识 Github actions
46
【架构师(第四十六篇)】 服务端开发之安装 Docker
47
【架构师(第四十七篇)】 服务端开发之认识 Docker
48
【架构师(第四十八篇)】 服务端开发之 Dockerfile
49
【架构师(第四十九篇)】 服务端开发之认识 Docker-compose
50
【架构师(第五十篇)】 服务端开发之自动发布到测试机
清单首页架构文章详情

【架构师(第三十篇)】Vue-Test-Utils 全局组件和第三方库 vuex | vue-router


测试所用代码

  • 使用了全局组件
    • a-button
    • a-menu
    • ... ...
  • 使用了外部的模块
    • useStore
    • useRouter
    • message
代码语言:javascript
复制
<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 方法的第二个参数中定义全局组件

代码语言: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');

// 模拟组件
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

代码语言:javascript
复制
// 先引入真实的 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

代码语言:javascript
复制
// 引入真实的 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');
    });
  });
});
下一篇
举报
领券