如何编写Vue3组件测试用例?

在做单元测试中最重要的就是编写单元测试用例。vue 项目的单元测试要测试什么呢? 今天主要来介绍下如何编写 vue3 组件的测试用例

vitest 为我们提供了几个常用的 Api

  1. describe()
    使用 describe 你可以在当前上下文中定义一个新套件,作为一组相关的测试或基准以及其他嵌套套件
  2. test()
    定义了一组关于测试期望的方法。它接收测试名称和一个含有测试期望的函数。
  3. expect()
    用来创建断言,描述测试期望

测试组件渲染

Vue Test Utils 提供两种渲染方式,mount 和 shallowMount。区别是 mount 会渲染子组件,shallowMount 把子组件渲染为 stub 组件。

假如单元测试用例不涉及子组件功能测试的话,使用 shallowMount 更合理。

describe("测试示例", () => {
  test("hello world", () => {
    const wrapper = shallowMount(HelloWorld);
    expect(wrapper.find(".name").text()).toBe("hello world!");
  });
});

由于不涉及子组件 此处用 shallowMount 更为合理

测试条件渲染

v-if

const Nav = {
  template: `
<nav>
    <a v-if="admin" id="admin" href="/admin">Admin</a>
  </nav>
`,
  data() {
    return { admin: false };
  },
};
describe("v-if测试示例", () => {
  test("v-if", () => {
    const wrapper = shallowMount(Nav);
    const admin = wrapper.find("#admin");
    expect(admin.exists()).toBe(false); // 期望admin节点不存在
  });
});

这里用 find()来查找这个节点 用 exists()来判断这个节点是否存在

v-show

const Nav = {
  template: `
<nav>
    <a id="user" href="/profile">My Profile</a>
    <ul v-show="shouldShowDropdown" id="user-dropdown">
      <!-- dropdown content -->
    </ul>
  </nav>
`,
  data() {
    return { shouldShowDropdown: false };
  },
};
describe("v-show测试示例", () => {
  test("v-show", () => {
    const wrapper = mount(Nav);
    expect(wrapper.get("#user-dropdown").isVisible()).toBe(false);
  });
});

isVisible()提供检查隐藏元素节点的能力,它能够检查以下内容

  1. 元素或其祖先元素拥有 display: none 或 visibility: hidden 或 opacity :0 样式
  2. 元素或其祖先元素在折叠的 details 标签中
  3. 元素或其祖先元素拥有 hidden 属性

测试组件的 props

<template>
  <div class="len">{{ minLength }}</div>
</template>

<script setup lang="ts">
const props = defineProps({
  minLength: {
    type: Number,
    default: 0,
  },
});
</script>
describe("props测试示例", () => {
  test("props", () => {
    const wrapper = shallowMount(Nav, {
      props: {
        minLength: 10,
      },
    });
    expect(wrapper.find(".len").text()).toBe("10");
  });
});

通过传递 props 的值在测试用例中使用

测试组件的 emit 事件

<template>
  <button @click="handleClick">Increment</button>
</template>

<script setup lang="ts">
import { ref } from "vue";

const count = ref(0);
const emit = defineEmits(["increment"]);
const handleClick = () => {
  count.value += 1;
  emit("increment", count.value);
};
</script>
import Counter from "./counter.vue";
describe("emit事件测试示例", () => {
  test("emits", () => {
    const wrapper = shallowMount(Counter);
    wrapper.get("button").trigger("click");
    expect(wrapper.emitted()).toHaveProperty("increment");
  });
});

当有 emit 事件触发时 wrapper.emitted()返回一个包含该事件名称作为属性的对象

{
  increment: [ [ 1 ] ],
  click: [ [ [MouseEvent] ] ]
}

测试组件的 slot

<template>
  <div>
    <slot></slot>
    <slot name="header"></slot>
    <slot name="scoped" v-bind="msg"></slot>
  </div>
</template>
describe("slot测试示例", () => {
  test("slot", () => {
    const wrapper = shallowMount(Layout, {
      slots: {
        // 默认插槽
        default: "message",
        // 具名插槽
        header: "123",
      },
    });
    expect(wrapper.text()).toContain("message");
    expect(wrapper.text()).toContain("123");
  });
  // 作用域插槽
  test("scoped slot", () => {
    const wrapper = shallowMount(Layout, {
      slots: {
        scoped: `<template #scoped="scope">
        Hello {{ scope.msg }}
        </template>
      `,
      },
    });
    expect(wrapper.text()).toContain("Hello world");
  });
});

测试 provide

const Provider = {
  setup() {
    provide("foo", 1);
    return () => h(Middle);
  },
};
const Middle = {
  render: () => h(Consumer),
};
const Consumer = {
  setup() {
    const foo = inject("foo");
    return () => h("div", { class: "inject-result" }, foo);
  },
};

describe("provide测试示例", () => {
  test("provides correct data", () => {
    const wrapper = mount(Provider);
    expect(wrapper.find(".inject-result").text()).toBe("1");
  });
});

构造爷孙组件利用 provide/inject 传参

表单测试

<template>
  <div>
    <input type="email" v-model="email" />
    <button @click="submit">Submit</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
const email = ref("");
const emit = defineEmits(["submit"]);
const submit = () => {
  emit("submit", email.value);
};
</script>
describe("表单测试示例", () => {
  test("登录", async () => {
    const wrapper = shallowMount(Login);
    const input = wrapper.find("input");

    await input.setValue("my@mail.com");
    expect(input.element.value).toBe("my@mail.com");

    await wrapper.find("button").trigger("click");
    expect(wrapper.emitted("submit")?.[0][0]).toBe("my@mail.com");
  });
});

通过 setValue 设置一个文本控件或 select 元素的值并更新 v-model 绑定的数据。

忽略某些代码

istanbul 提供注释语法,允许某些代码不计入覆盖率。

// 忽略一个 if 分支
/* istanbul ignore if */
if (hardToReproduceError)) {
    return callback(hardToReproduceError);
}
// 忽略一个 else 分支
/* istanbul ignore else */
if (foo.hasOwnProperty("bar")) {
  // do something
}
// 忽略默认值 {}
var object = parameter || /* istanbul ignore next */ {};

/* istanbul ignore file */
// 写在文件头部 忽略整个文件

参考文章:

  1. Vitest API 索引
  2. Vue Test Utils
  3. Expect 断言
  4. Vue Testing Handbook
  5. 看懂「测试覆盖率报告」