在我们开发完成一些功能相对稳定的组件时,对应地增加自动化测试有以下好处。
-
确保组件功能的正确性
单元测试能够验证组件的各个功能是否按预期工作。通过测试,可以确保组件在不同场景下的行为一致,避免因代码改动引入错误。
-
提升代码质量
编写测试促使开发者编写更模块化、可维护的代码,因为只有结构清晰的代码才易于测试。同时,测试也能帮助发现潜在的设计问题。
-
减少回归问题
当项目规模增大或多人协作时,修改代码可能引发意外问题。单元测试能快速发现这些回归问题,确保新改动不会破坏已有功能。
-
增强开发信心
通过测试,开发者可以更有信心地重构或优化代码,因为测试能够及时捕捉到错误,减少手动测试的工作量。
-
促进团队协作
测试代码作为文档,帮助其他开发者理解组件的预期行为,减少沟通成本,提升团队协作效率。
-
自动化测试
单元测试可以集成到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
是控制 import
或 require
解析 package.json
的 exports
字段,影响 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-utils
的mount
方法创建一个包含已挂载和渲染的 Vue 组件的 Wrapper 以进行测试,并给组件传递props为{num: 3}
。
2、通过get
方法获取到count
所在的元素,期望该元素里的文本为3。
3、获取button
元素,点击。
4、期望该元素的文本为4。
在package.json
文件中添加
json
"scripts": {
"test": "jest"
},
跑下npm run test
即可看到对应的结果

可以看到,测试是通过的。
查找
get
上面的例子查找元素用到了get
,get
获取一个元素,如果找到则返回一个 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");
});
运行后会报错

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
find
和get
类似,其语法和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()
方法。它接受一个参数,通常是一个 String
或 Boolean
,并返回一个 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,
}); // 检查参数是否正确
});