Vue组件测试的常见场景

155 阅读5分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情

image.png

一、前言

1. 测试类型

前端中的测试主要分三种:单元测试,集成测试,端到端测试:

单元测试集成测试端对端测试
测试对象代码单元(函数,模块等)组织在一起的代码单元整个页面或应用
作用保证代码单元在给给定输入下得到预期结果保证多个模块组织在一起可以正常工作模拟用户在真实环境中的操作,确保一切正常
测试环境非浏览器环境非浏览器环境浏览器环境
代表工具JestMochaKarmaJest, Testing LibraryCypressPuppeteer, NightWatch

但是在实际测试中,单元测试和集成测试之间的区别可能很难区分。正如React官网所言:

对组件来说,“单元测试”和“集成测试”之间的差别可能会很模糊。如果你在测试一个表单,用例是否应该也测试表单里的按钮呢?一个按钮组件又需不需要有他自己的测试套件?重构按钮组件是否应该影响表单的测试用例?不同的团队或产品可能会得出不同的答案。

因此对前端而言,如果一定要分为三种类型的测试,会过于繁琐,同时也没要必要性。目前普遍的情况是大家做两个类型的测试:单元测试,E2E 测试。这里的单元测试,可以认为包括集成测试。

2. 技术选型

Vue-Test-UtilsVue的官方的单元测试框架,它提供了一系列非常方便的工具,使我们更加轻松的为Vue构建的应用来编写单元测试。主流的 JavaScript 测试运行器有很多,但 Vue Test Utils 都能够支持。它是测试运行器无关的。

Jest是一个由 Facebook 开发的测试运行器,也是Vue推荐的测试运行器之一。相对其他测试框架,其特点就是就是内置了常用的测试工具,比如自带断言、测试覆盖率工具,实现了开箱即用。 此外, Jest 的测试用例是并行执行的,而且只执行发生改变的文件所对应的测试,提升了测试速度

这里我们介绍的就是 Vue-Test-Utils + Jest 结合的示例。

二、环境搭建

由于之前已经介绍过如何向已有Vue项目中自己引入并配置单元测试环境,这篇文章中就演示如何使用脚手架在创建新项目的时候就引入Jest,安装时的选项如下:

image.png

这样安装的好处一个是我们不用再额外安装Vue-Test-Utils,另一个是不用自己再配置Jest了(比如别名、Babel、文件类型相关的),脚手架会帮我们全部解决。

安装完成后,在命令行运行npm run test:unit,可以看到默认的测试用例通过:

image.png

下面的测试中我们就不对项目目录任何改动了,只关注测试本身,所以被测代码会被写在HelloWorld.vue中,测试用例编写在example.spec.js

三、编写测试用例

0. 被测组件

// HelloWorld.vue
<template>
  <div class="hello">
    <h2>{{ msg }}</h2>
    <span v-show="showSpan">Parent</span>
    <child v-if="showChild" @myEvent="onEvent"></child>
    <p>{{ numbers }}</p>
    <p v-if="emitted">Emitted!</p>
    <p>答案:{{ answer }}</p>
    <img :src="src" />
  </div>
</template>

<script>
import Child from "@/components/Child.vue";
import axios from "axios";

export default {
  name: "HelloWorld",
  components: {
    Child,
  },
  props: {
    msg: {
      type: String,
      default: "Vue and Jest",
    },
    even: {
      type: Boolean,
      default: true,
    },
  },

  data() {
    return {
      greeting: "Hello Vue and Jest",
      showSpan: true,
      showChild: true,
      emitted: false,
      answer: "",
      src: "",
    };
  },
  computed: {
    numbers() {
      const evens = [];
      const odds = [];

      for (let i = 1; i < 10; i++) {
        if (i % 2 === 0) {
          evens.push(i);
        } else {
          odds.push(i);
        }
      }
      return this.even === true ? evens.join(", ") : odds.join(", ");
    },
  },
  mounted() {
    this.getAnswer();
  },
  methods: {
    onEvent() {
      this.emitted = true;
    },
    getAnswer() {
      const URL = "https://yesno.wtf/api";
      return axios
        .get(URL)
        .then((res) => {
          if (res && res.data) {
            this.answer = res.data.answer;
            this.src = res.data.image;
            return res.data;
          }
        })
        .catch((err) => {
          console.log(err);
        });
    },
  },
};
</script>

主要逻辑如下:

  • HelloWord为父组件,Child为子组件,子组件内容的可见与否由showChild变量决定
  • HelloWord组件挂载完会通过axios发送请求,返回的数据显示在页面pimg
  • HelloWord组件的computed方法根据even的值决定返回奇数列还是偶数列
  • Child组件中自定义了myEvent事件,代码如下所示:
// Child.vue

<template>
  <div>
    <h3>Message from Child</h3>
  </div>
</template>

<script>
export default {
  name: "Child",
  methods: {
    emitEvent() {
      this.$emit("myEvent", "name", "password");
    },
  },
};
</script>

1. 测试DOM元素

import { mount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";
  
test("测试DOM元素", () => {
    const wrapper = mount(HelloWorld);
    expect(wrapper.find("span").isVisible()).toBe(true); // isVisible()用来断言v-show所控制元素是否可见
});

2. 测试组件渲染

import { mount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";

test("测试组件渲染", () => {
    const wrapper = mount(HelloWorld);
    expect(wrapper.findComponent(Child).exists()).toBe(true); // exists()用来断言v-if所控制元素是否存在
});

3. 测试data

import { mount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";

test("测试data", () => {
    const wrapper = mount(HelloWorld, {
      data() {
        return {
          showSpan: false,
          showChild: false,
        };
      },
    });
    expect(wrapper.find("span").isVisible()).toBe(false);
    expect(wrapper.findComponent(Child).exists()).toBe(false);
});

4. 测试computed

  import { mount } from "@vue/test-utils";
  import HelloWorld from "@/components/HelloWorld.vue";

  test("测试computed", () => {
    const wrapper = mount(HelloWorld, {
      propsData: {
        even: true,
      },
    });
    expect(wrapper.find("p").text()).toBe("2, 4, 6, 8");
  });

5. 测试props

  import { mount } from "@vue/test-utils";
  import HelloWorld from "@/components/HelloWorld.vue";
  
  it("测试props", () => {
    const msg = "new message";
    const wrapper = mount(HelloWorld, {
      propsData: { msg },
    });
    expect(wrapper.find("h2").text()).toBe("new message");
  });

6. 测试emit事件

  import { mount } from "@vue/test-utils";
  import Child from "@/components/Child.vue";
  
  it("测试emit事件", () => {
    const wrapper = mount(Child);
    // 通过wrapper.vm属性访问一个实例所有的方法和属性(可以当成组件中的this)
    wrapper.vm.emitEvent();
    // wrapper会自动记录挂载实例的发射事件
    expect(wrapper.emitted().myEvent[0]).toEqual(["name", "password"]);
  });

7. 测试axios请求

import { mount } from "@vue/test-utils";
import HelloWorld from "@/components/HelloWorld.vue";
import Child from "@/components/Child.vue";
import axios from "axios";

jest.mock("axios"); // 必须写在最外层

test("测试axios请求", async () => {
    // 模拟请求的返回值
    axios.get.mockResolvedValue({
      data: {
        name: "Yiler",
        year: 2022,
      },
      status: "success",
    });
    const getSpy = jest.spyOn(axios, "get");
    const wrapper = mount(HelloWorld);
  
    await wrapper.vm.getAnswer().then((data) => {
      expect(data).toEqual({
        name: "Yiler",
        year: 2022,
      });
    });
    expect(getSpy).toHaveBeenCalledTimes(2); // 组件挂载的时候调用了一次,await wrapper.vm.getAnswer()又调用了一次
  });