用Jest和Vue Test Utils给Vue组件写单测

在我们开发完成一些功能相对稳定的组件时,对应地增加自动化测试有以下好处。

  • 确保组件功能的正确性

    单元测试能够验证组件的各个功能是否按预期工作。通过测试,可以确保组件在不同场景下的行为一致,避免因代码改动引入错误。

  • 提升代码质量

    编写测试促使开发者编写更模块化、可维护的代码,因为只有结构清晰的代码才易于测试。同时,测试也能帮助发现潜在的设计问题。

  • 减少回归问题

    当项目规模增大或多人协作时,修改代码可能引发意外问题。单元测试能快速发现这些回归问题,确保新改动不会破坏已有功能。

  • 增强开发信心

    通过测试,开发者可以更有信心地重构或优化代码,因为测试能够及时捕捉到错误,减少手动测试的工作量。

  • 促进团队协作

    测试代码作为文档,帮助其他开发者理解组件的预期行为,减少沟通成本,提升团队协作效率。

  • 自动化测试

    单元测试可以集成到CI/CD流程中,自动运行测试用例,确保每次提交的代码都经过验证,减少人工测试的负担。

  • 提高代码覆盖率

    通过单元测试,可以检查代码的覆盖率,确保大部分代码逻辑都经过测试,减少未测试代码带来的风险。

  • 支持TDD开发

    单元测试支持测试驱动开发(TDD),先写测试再写实现代码,确保代码从一开始就符合预期,减少后期调试时间。

由此可见写单元测试非常重要,那么,如何写Vue的组件单测呢?

可以使用jest测试框架和Vue Test Utils结合来写。

Vue Test Utils 是vue官方提供的测试工具库,提供了一系列方便的API。

jest测试框架的基本使用可以参照笔者的另一篇文章:juejin.cn/post/745746...)

搭建项目

从零开始,新建一个文件夹并执行npm init初始化项目

安装相关依赖

vue 相关依赖

  • vue
  • @vue/test-utils
  • @vue/vue3-jest
javascript 复制代码
npm install vue
javascript 复制代码
npm install @vue/test-utils @vue/vue3-jest -D

jest 相关依赖

  • jest
  • jest-environment-jsdom
javascript 复制代码
npm install jest jest-environment-jsdom -D

babel 相关依赖

  • babel-jest
  • @babel/preset-env
  • @babel/preset-typescript
javascript 复制代码
npm install babel-jest @babel/preset-env @babel/preset-typescript -D

添加相关配置文件

添加babel配置文件

添加 babel.config.json

json 复制代码
{
  "presets": [
    ["@babel/preset-env", { "targets": { "node": "current" } }],
    "@babel/preset-typescript"
  ]
}

添加jest配置文件

添加jest.config.js

javascript 复制代码
export default {
  testEnvironmentOptions: {
    customExportConditions: ["node", "node-addons"],
  },
  transform: {
    "^.+\\.vue$": "@vue/vue3-jest",
    "^.+\\.(js|jsx|ts|tsx)$": "babel-jest",
  },
  moduleFileExtensions: ["js", "jsx", "ts", "tsx", "json", "vue"],
  testEnvironment: "jsdom", // 使用 jsdom 模拟浏览器环境
};

其中的testEnvironmentOptions允许你自定义测试环境的额外配置 。它的值取决于 testEnvironment,比如:

  • 如果 testEnvironment: "jsdom"testEnvironmentOptions 影响"JSDOM"。
  • 如果 testEnvironment: "node"testEnvironmentOptions 影响"Node.js 运行环境"。

customExportConditions是控制 importrequire 解析 package.jsonexports 字段,影响 Jest 解析模块的方式。

在 Node.js 里,很多 npm 包使用 exports 字段 来指定不同的环境,比如:

json 复制代码
{
  "exports": {
    "import": "./esm/index.mjs",
    "require": "./cjs/index.cjs",
    "node": "./node/index.js"
  }
}

默认情况下,Jest 可能解析 import,导致 esm/cjs 冲突。

加上 customExportConditions: ["node", "node-addons"],Jest 强制使用 Node.js 版本的代码,避免兼容性问题。

组件测试

先写一个简单的Vue组件

vue 复制代码
<template>
  <div>
    <p className="count-box">{{ count }}</p>
    <button @click="add">add</button>
  </div>
</template>

<script setup lang="ts">
import { ref } from "vue";
const props = defineProps({
  num: {
    type: Number,
    default: 0,
  },
});

const count = ref(props.num);
const add = () => {
  count.value++;
};
</script>

再写一个测试

javascript 复制代码
import { mount } from "@vue/test-utils";
import MyComponent from "./index.vue";

test("add", async () => {
  const wrapper = mount(MyComponent, {
    props: {
      num: 3,
    },
  });
  const countDom = wrapper.get(".count-box");
  expect(countDom.text()).toBe("3");
  const button = wrapper.get("button");
  await button.trigger("click");
  expect(countDom.text()).toBe("4");
});

测试的流程大概是

1、通过@vue/test-utilsmount方法创建一个包含已挂载和渲染的 Vue 组件的 Wrapper 以进行测试,并给组件传递props为{num: 3}

2、通过get方法获取到count所在的元素,期望该元素里的文本为3。

3、获取button元素,点击。

4、期望该元素的文本为4。

package.json文件中添加

json 复制代码
"scripts": {
    "test": "jest"
},

跑下npm run test即可看到对应的结果

![image-20250211165212242](/Users/chenkai/Desktop/:Users:chenkai:Library:Application Support:typora-user-images:image-20250211165212242.png)

可以看到,测试是通过的。

查找

get

上面的例子查找元素用到了getget获取一个元素,如果找到则返回一个 DOMWrapper,否则抛出错误。

假如把上面的例子改成下面这样

javascript 复制代码
import { mount } from "@vue/test-utils";
import MyComponent from "./index.vue";

test("add", async () => {
  const wrapper = mount(MyComponent, {
    props: {
      num: 3,
    },
  });
  const countDom = wrapper.get(".my-box"); // 修改成一个不存在的classname
  expect(countDom.text()).toBe("3");
});

运行后会报错

![image-20250211183248607](/Users/chenkai/Desktop/:Users:chenkai:Library:Application Support:typora-user-images:image-20250211183248607.png)

get的语法和querySelector相同,此外还可以搜索元素的ref引用。

javascript 复制代码
import { mount } from "@vue/test-utils";

// 要测试的组件
const MyComponent = {
  template: `
    <div id="div" class="div" data-test="div" ref="div">div</div>
  `,
};

test("get", () => {
  const wrapper = mount(MyComponent);

  expect(wrapper.get("#div").text()).toBe("div");
  expect(wrapper.get(".div").text()).toBe("div");
  expect(wrapper.get("div").text()).toBe("div");
  expect(wrapper.get('[data-test="div"]').text()).toBe("div");
  expect(wrapper.get({ ref: "div" }).text()).toBe("div");
});

getComponent

获取 Vue 组件实例,如果找到则返回一个 VueWrapper,否则抛出错误。

getComponent支持的语法有

  • querySelector相同的语法,匹配标准查询选择器。
  • 组件名称,匹配 PascalCase、snake-case 和 camelCase。
  • 组件ref引用,仅可用于已挂载组件的直接引用子组件。
  • 单文件组件(SFC),直接传入导入的组件。
vue 复制代码
<!-- myComponent.vue -->
<template>
  <div class="div">my_component</div>
</template>

<script setup lang="ts">
defineOptions({
  name: "MyComponent",
});
</script>
vue 复制代码
<!-- testComponent.vue -->
<template>
  <MyComponent
    data-test="my_component"
    ref="my_component"
    id="my_component"
    class="my_component"
  >
  </MyComponent>
</template>

<script setup lang="ts">
import MyComponent from "./myComponent.vue";
</script>
javascript 复制代码
import { mount } from "@vue/test-utils";
import TestComponent from "./testComponent.vue";
import MyComponent from "./myComponent.vue";

test("getComponent", () => {
  const wrapper = mount(TestComponent);

  expect(wrapper.getComponent("#my_component").text()).toBe("my_component");
  expect(wrapper.getComponent(".my_component").text()).toBe("my_component");
  expect(wrapper.getComponent('[data-test="my_component"]').text()).toBe(
    "my_component"
  );
  expect(wrapper.getComponent({ name: "MyComponent" }).text()).toBe(
    "my_component"
  );
  expect(wrapper.getComponent({ ref: "my_component" }).text()).toBe(
    "my_component"
  );
  expect(wrapper.getComponent(MyComponent).text()).toBe("my_component");
});

find

findget类似,其语法和get一样,但如果未找到元素,find 将返回一个 ErrorWrapper,而get会抛出一个错误。

根据经验,当你断言某个元素不存在时,请始终使用 find。如果你断言某个元素确实存在,请使用 get

javascript 复制代码
import { mount } from "@vue/test-utils";

// 要测试的组件
const MyComponent = {
  template: `
    <div id="div" class="div" data-test="div" ref="div">div</div>
  `,
};

test("find", () => {
  const wrapper = mount(MyComponent);

  expect(wrapper.find("#div").text()).toBe("div");
  expect(wrapper.find(".div").text()).toBe("div");
  expect(wrapper.find("div").text()).toBe("div");
  expect(wrapper.find('[data-test="div"]').text()).toBe("div");
  expect(wrapper.find({ ref: "div" }).text()).toBe("div");
  expect(wrapper.find(".no_there").exists()).toBe(false);
});

findAll

find 类似,但返回的是一个 DOMWrapper 数组。

vue 复制代码
<!-- myComponent.vue -->
<template>
  <div v-for="num in [1, 2, 3]" :key="num">
    {{ num }}
  </div>
</template>
javascript 复制代码
import { mount } from "@vue/test-utils";
import MyComponent from "./myComponent.vue";

test("findAll", () => {
  const wrapper = mount(MyComponent);

  expect(wrapper.findAll("div")[0].text()).toBe("1");
  expect(wrapper.findAll("div")[1].text()).toBe("2");
  expect(wrapper.findAll("div")[2].text()).toBe("3");
});

findComponent

找到一个 Vue 组件实例并返回一个 VueWrapper (如果找到)。否则返回 ErrorWrapper

getComponent 类似,但如果未找到 Vue 组件实例,getComponent 会抛出错误,而 findComponent 会返回一个 ErrorWrapper。

vue 复制代码
<!-- myComponent.vue -->
<template>
  <div class="div">my_component</div>
</template>

<script setup lang="ts">
defineOptions({
  name: "MyComponent",
});
</script>
vue 复制代码
<!-- testComponent.vue -->
<template>
  <MyComponent
    data-test="my_component"
    ref="my_component"
    id="my_component"
    class="my_component"
  >
  </MyComponent>
</template>

<script setup lang="ts">
import MyComponent from "./myComponent.vue";
</script>
javascript 复制代码
import { mount } from "@vue/test-utils";
import TestComponent from "./testComponent.vue";
import MyComponent from "./myComponent.vue";

test("findComponent", () => {
  const wrapper = mount(TestComponent);

  expect(wrapper.findComponent("#my_component").text()).toBe("my_component");
  expect(wrapper.findComponent(".my_component").text()).toBe("my_component");
  expect(wrapper.findComponent('[data-test="my_component"]').text()).toBe(
    "my_component"
  );
  expect(wrapper.findComponent({ name: "MyComponent" }).text()).toBe(
    "my_component"
  );
  expect(wrapper.findComponent({ ref: "my_component" }).text()).toBe(
    "my_component"
  );
  expect(wrapper.findComponent(MyComponent).text()).toBe("my_component");
  expect(wrapper.findComponent({ name: "noThere" }).exists()).toBe(false);
});

findAllComponents

findComponent 类似(ref 语法在 findAllComponents 中不支持。所有其他查询语法都是有效的。),但查找所有匹配查询的 Vue 组件实例。返回一个 VueWrapper 数组。

vue 复制代码
<!-- myComponent.vue -->
<template>
  <div class="div">{{ props.num }}</div>
</template>

<script setup lang="ts">
const props = defineProps(["num"]);
defineOptions({
  name: "MyComponent",
});
</script>
vue 复制代码
<!-- testComponent.vue -->
<template>
  <MyComponent v-for="num in [1, 2, 3]" :key="num" :num="num"> </MyComponent>
</template>

<script setup lang="ts">
import MyComponent from "./myComponent.vue";
</script>
javascript 复制代码
import { mount } from "@vue/test-utils";
import TestComponent from "./testComponent.vue";
import MyComponent from "./myComponent.vue";

test("findAllComponents", () => {
  const wrapper = mount(TestComponent);

  expect(wrapper.findAllComponents(MyComponent)[0].text()).toBe("1");
  expect(wrapper.findAllComponents(MyComponent)[1].text()).toBe("2");
  expect(wrapper.findAllComponents(MyComponent)[2].text()).toBe("3");
});

事件

这是一个表单组件,填写信息后点击按钮调用父组件事件提交。

vue 复制代码
<!-- Form.vue -->
<template>
  <div>
    <input type="email" v-model="email" />
    <select v-model="sex">
      <option value="male">male</option>
      <option value="female">female</option>
    </select>
    <input type="checkbox" v-model="subscribe" />
    <button @click="submit">submit</button>
  </div>
</template>

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

const emits = defineEmits(["submit"]);

const email = ref("");
const sex = ref("");
const subscribe = ref(false);

const submit = () => {
  emits("submit", {
    email: email.value,
    sex: sex.value,
    subscribe: subscribe.value,
  });
};
</script>

为了测试这个组件,需要验证是否触发了正确参数的"submit"事件。

为了完成这个验证我们需要在测试代码中实现以下

1、修改表单的值

2、触发事件

3、获取事件数据

要修改组件中表单的值,可以使用setValue()方法。它接受一个参数,通常是一个 StringBoolean,并返回一个 Promise,在 Vue 更新 DOM 后完成解析。

触发事件,我们可以使用 trigger 方法。

断言触发的事件,我们可以使用 emitted() 方法。它返回一个对象,包含组件触发的所有事件,其参数以数组的形式呈现。

javascript 复制代码
import { mount } from "@vue/test-utils";
import Form from "./Form.vue";

test("emits an event when clicked", async () => {
  const wrapper = mount(Form);

  const emailInput = wrapper.find("input[type=email]");
  const sexSelect = wrapper.find("select");
  const subscribeInput = wrapper.find("input[type=checkbox]");

  await emailInput.setValue("aaa@aa.com");
  await sexSelect.setValue("male");
  await subscribeInput.setValue(true);

  expect(emailInput.element.value).toBe("aaa@aa.com");
  expect(sexSelect.element.value).toBe("male");
  expect(subscribeInput.element.checked).toBe(true);

  const button = wrapper.find("button");
  button.trigger("click");

  expect(wrapper.emitted()).toHaveProperty("submit");

  const submitEvent = wrapper.emitted("submit");
  expect(submitEvent).toHaveLength(1); // 触发了一次,所以数组只有一个值
  expect(submitEvent[0][0]).toEqual({
    email: "aaa@aa.com",
    sex: "male",
    subscribe: true,
  }); // 检查参数是否正确
});

如上面所示,emitted() 还可以接受一个参数。它返回一个包含所有 this.$emit('submit') 发生情况的数组,其中每个元素代表一个已触发的事件。我们可以通过它验证事件的触发次数及参数。

我们也可以测试点击多次的情况

javascript 复制代码
import { mount } from "@vue/test-utils";
import Form from "./Form.vue";

test("emits an event when clicked", async () => {
  const wrapper = mount(Form);

  const emailInput = wrapper.find("input[type=email]");
  const sexSelect = wrapper.find("select");
  const subscribeInput = wrapper.find("input[type=checkbox]");
  const button = wrapper.find("button");

  await emailInput.setValue("aaa@aa.com");
  await sexSelect.setValue("male");
  await subscribeInput.setValue(true);

  button.trigger("click");

  await emailInput.setValue("bbb@bb.com");
  await sexSelect.setValue("female");

  button.trigger("click");

  expect(wrapper.emitted()).toHaveProperty("submit");
  const submitEvent = wrapper.emitted("submit");
  expect(submitEvent).toHaveLength(2); // 触发了两次次,所以数组有两个值
  expect(submitEvent[0][0]).toEqual({
    email: "aaa@aa.com",
    sex: "male",
    subscribe: true,
  }); // 检查参数是否正确
  expect(submitEvent[1][0]).toEqual({
    email: "bbb@bb.com",
    sex: "female",
    subscribe: true,
  }); // 检查参数是否正确
});
相关推荐
天宇&嘘月2 小时前
web第三次作业
前端·javascript·css
小王不会写code2 小时前
axios
前端·javascript·axios
发呆的薇薇°3 小时前
vue3 配置@根路径
前端·vue.js
luoluoal3 小时前
基于Spring Boot+Vue的宠物服务管理系统(源码+文档)
vue.js·spring boot·宠物
luckyext3 小时前
HBuilderX中,VUE生成随机数字,vue调用随机数函数
前端·javascript·vue.js·微信小程序·小程序
小小码农(找工作版)3 小时前
JavaScript 前端面试 4(作用域链、this)
前端·javascript·面试
前端没钱4 小时前
前端需要学习 Docker 吗?
前端·学习·docker
前端郭德纲4 小时前
前端自动化部署的极简方案
运维·前端·自动化
海绵宝宝_4 小时前
【HarmonyOS NEXT】获取正式应用签名证书的签名信息
android·前端·华为·harmonyos·鸿蒙·鸿蒙应用开发
码农土豆5 小时前
chrome V3插件开发,调用 chrome.action.setIcon,提示路径找不到
前端·chrome