持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第20天,点击查看活动详情
一、前言
1. 测试类型
前端中的测试主要分三种:单元测试,集成测试,端到端测试:
| 单元测试 | 集成测试 | 端对端测试 | |
|---|---|---|---|
| 测试对象 | 代码单元(函数,模块等) | 组织在一起的代码单元 | 整个页面或应用 |
| 作用 | 保证代码单元在给给定输入下得到预期结果 | 保证多个模块组织在一起可以正常工作 | 模拟用户在真实环境中的操作,确保一切正常 |
| 测试环境 | 非浏览器环境 | 非浏览器环境 | 浏览器环境 |
| 代表工具 | Jest,Mocha,Karma等 | Jest, Testing Library等 | Cypress,Puppeteer, NightWatch等 |
但是在实际测试中,单元测试和集成测试之间的区别可能很难区分。正如React官网所言:
对组件来说,“单元测试”和“集成测试”之间的差别可能会很模糊。如果你在测试一个表单,用例是否应该也测试表单里的按钮呢?一个按钮组件又需不需要有他自己的测试套件?重构按钮组件是否应该影响表单的测试用例?不同的团队或产品可能会得出不同的答案。
因此对前端而言,如果一定要分为三种类型的测试,会过于繁琐,同时也没要必要性。目前普遍的情况是大家做两个类型的测试:单元测试,E2E 测试。这里的单元测试,可以认为包括集成测试。
2. 技术选型
Vue-Test-Utils是Vue的官方的单元测试框架,它提供了一系列非常方便的工具,使我们更加轻松的为Vue构建的应用来编写单元测试。主流的 JavaScript 测试运行器有很多,但 Vue Test Utils 都能够支持。它是测试运行器无关的。
Jest是一个由 Facebook 开发的测试运行器,也是Vue推荐的测试运行器之一。相对其他测试框架,其特点就是就是内置了常用的测试工具,比如自带断言、测试覆盖率工具,实现了开箱即用。
此外, Jest 的测试用例是并行执行的,而且只执行发生改变的文件所对应的测试,提升了测试速度
这里我们介绍的就是 Vue-Test-Utils + Jest 结合的示例。
二、环境搭建
由于之前已经介绍过如何向已有Vue项目中自己引入并配置单元测试环境,这篇文章中就演示如何使用脚手架在创建新项目的时候就引入Jest,安装时的选项如下:
这样安装的好处一个是我们不用再额外安装Vue-Test-Utils,另一个是不用自己再配置Jest了(比如别名、Babel、文件类型相关的),脚手架会帮我们全部解决。
安装完成后,在命令行运行npm run test:unit,可以看到默认的测试用例通过:
下面的测试中我们就不对项目目录任何改动了,只关注测试本身,所以被测代码会被写在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发送请求,返回的数据显示在页面p和img中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()又调用了一次
});