zl程序教程

您现在的位置是:首页 >  Java

当前栏目

【架构师(第二十九篇)】Vue-Test-Utils 触发事件和异步请求

2023-02-18 15:36:23 时间

知识点

  • mock 对象断言为特定类型 使用 jest.Mocked<T>
  • 使用 it.only 来指定测试的 case
  • 使用 skip 跳过指定测试的 case

测试内容

  • 触发事件
    • trigger 方法
  • 测试界面是否更新
    • 特别注意 DOM 更新是个异步的过程
    • 使用 async await
  • 更新表单
    • setValue 方法
  • 验证事件是否发送
    • emitted 方法
  • 测试异步请求
    • 模拟第三方库实现

测试准备和结束

可以使用内置的一些钩子来简化一些通用的逻辑,以下钩子用于一次性完成测试准备。

  • beforeAll
  • afterAll
let wrapper: VueWrapper<any>;
describe('HelloWorld.vue', () => {
  // 在多个 case 运行之前执行,只执行一次,由于这样会让所有的用例使用一个 `warpper` 实例,可能会造成错误。
  beforeAll(() => {
    // 获取组件
    wrapper = shallowMount(HelloWorld, {
      props: { msg },
    });
  });
  // 在多个 case 运行之后执行,只执行一次
  afterAll(() => {});
});

以下钩子用于每个测试用例测试准备。

  • beforeEach
  • afterEach
let wrapper: VueWrapper<any>;
describe('HelloWorld.vue', () => {
  beforeEach(() => {
    // 获取组件
    wrapper = shallowMount(HelloWorld, {
      props: { msg },
    });
  });
  afterEach(() => {
    mockAxios.get.mockReset();
  });
});

测试建议

如果一个测试失败了,需要注意

  • 它是否是唯一在运行的用例,使用 only 单独运行一次
  • 如果单独运行没问题,整体运行出错,应该检查 beforeEachbeforeAll 等全局钩子中的逻辑是否有问题,判断是否需要清空共享状态。

测试组件

父组件

<template>
  <div>
    <!-- 显示 props.msg 内容 -->
    <h1>{{ msg }}</h1>
    <!-- 按钮 点击 count ++  -->
    <button class="add-count"
            @click="addCount">{{ count }}</button>
    <!-- 输入框 -->
    <input type="text"
           v-model="todo" />
    <!-- 按钮 点击添加 todo -->
    <button class="add-todo"
            @click="addTodo">addTodo</button>
    <!-- . todos 列表 渲染 todo -->
    <ul>
      <li v-for="(todo, index) in todos"
          :key="index">{{ todo }}</li>
    </ul>
    <!-- 按钮 点击发起异步请求 -->
    <button class="load-user"
            @click="loadUser">loadUser</button>
    <!-- 加载动画 -->
    <p v-if="user.loading"
       class="loading">loading</p>
    <!-- 显示数据 -->
    <div v-else
         class="user-name">{{ user?.data?.username }}</div>
    <!-- 错误提示 -->
    <p v-if="user.error"
       class="error">error</p>
    <!-- 子组件 传递 msg = 1234 -->
    <hello-com msg="1234"></hello-com>
  </div>
</template>

<script setup lang="ts">
import axios from 'axios'
import { defineProps, ref, defineEmits, reactive } from 'vue'
import HelloCom from './hello.vue'

// 定义props
defineProps({
  msg: String
})

// 定义事件
const emit = defineEmits(['send'])

// 初始化 count
const count = ref(0)

// count++
const addCount = () => {
  count.value++
}

// 初始化 input 内容
const todo = ref('')

// 初始化 todos 列表
const todos = ref<string[]>([])

// 添加 todo 到 todos 列表
const addTodo = () => {
  if (todo.value) {
    todos.value.push(todo.value)
    emit('send', todo.value)
  }
}

// 初始化异步请求数据
const user = reactive({
  data: null as any,
  loading: false,
  error: false
})

// 异步请求
const loadUser = () => {
  user.loading = true
  axios.get("https://jsonplaceholder.typicode.com/users/1").then((resp) => {
    console.log(resp)
    user.data = resp.data
  }).catch(() => {
    user.error = true
  }).finally(() => {
    user.loading = false
  })
}
</script>

子组件

<template>
  <h1>{{ msg }}</h1>
</template>

<script setup lang="ts">
import { defineProps } from 'vue'
const props = defineProps({
  msg: String
})
</script>

测试代码

Dom 更新为异步操作,需要使用 async await

import axios from 'axios';
import flushPromises from 'flush-promises';
import type { VueWrapper } from '@vue/test-utils';
import { shallowMount } from '@vue/test-utils';
import HelloWorld from '@/components/HelloWorld.vue';

jest.mock('axios');
//将 mock 对象断言为特定类型 使用 jest.Mocked<T>
const mockAxios = axios as jest.Mocked<typeof axios>;
const msg = 'new message';
let wrapper: VueWrapper<any>;
describe('HelloWorld.vue', () => {
  beforeEach(() => {
    // 获取组件
    wrapper = shallowMount(HelloWorld, {
      props: { msg },
    });
  });
  afterEach(() => {
    mockAxios.get.mockReset();
  });
  // 测试点击 button, count 增加
  it('should update the count when clicking the button', async () => {
    // 触发点击事件
    await wrapper.get('.add-count').trigger('click');
    // 数字变为 1 (初始为0)
    expect(wrapper.get('.add-count').text()).toBe('1');
  });
  // 测试 更新表单 点击 add button
  it('should add todo when fill the input and click the add button', async () => {
    const todoContent = 'test todo';
    // 触发 input 事件 , 设置值为 todoContent
    await wrapper.get('input').setValue(todoContent);
    // 断言 input 的值为 todoContent
    expect(wrapper.get('input').element.value).toBe(todoContent);
    // 触发 button 点击事件
    await wrapper.get('.add-todo').trigger('click');
    // 断言 有一个 li
    expect(wrapper.findAll('li')).toHaveLength(1);
    // 断言 li 的内容是 todoContent
    expect(wrapper.get('li').text()).toBe(todoContent);
    // 断言 触发了 名为 send 的 emit 事件
    expect(wrapper.emitted()).toHaveProperty('send');
    // 获取 send 事件的 对象
    const events = wrapper.emitted('send')!;
    // 检查对象内容是否相同使用 toEqual, toBe 要求引用也相同
    expect(events[0]).toEqual([todoContent]);
  });
  // 使用 it.only 来指定测试的 case
  it('should load user message when click the load button', async () => {
    // mock service
    mockAxios.get.mockResolvedValueOnce({
      data: {
        username: 'warbler',
      },
    });
    // 触发点击事件
    await wrapper.get('.load-user').trigger('click');
    // 断言 请求是否被调用
    expect(mockAxios.get).toHaveBeenCalled();
    // 断言 loading 是否出现
    expect(wrapper.find('.loading').exists()).toBeTruthy();
    // 让 promise 完成,并且界面更新完成
    await flushPromises();
    // 断言 loading 消失
    expect(wrapper.find('.loading').exists()).toBeFalsy();
    // 断言 username 显示
    expect(wrapper.get('.user-name').text()).toBe('warbler');
  });
});

测试结果