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

2022-12-10 13:41:57 浏览数 (1)


测试所用代码

  • 使用了全局组件
    • 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');
    });
  });
});

0 人点赞