Jest
大约 15 分钟
前端单元测试
插件安装
推荐安装 Jest 、JestRunner、Jest Snippets 插件(vscode)

安装
注意:要将 npm 切换为淘宝源,否则安装容易报错,且 node 版本为 14 以上
npm config set registry https://registry.npm.taobao.org
命令行:推荐直接 copy 对应依赖及版本
npm install --save jest @vue/test-utils babel-jest vue-jest babel-jest…………
"dependencies": {
......
"@vue/test-utils": "^1.0.3",
"babel-jest": "^29.6.2",
"babel-core": "^7.0.0-bridge.0",
"babel-plugin-import": "^1.13.3",
"@vue/vue2-jest": "^29.2.5",
"jsdom": "^22.1.0",
"jsdom-global": "^3.0.2",
"jest": "^29.6.2",
"vue-jest": "^3.0.7"
}
Jest 配置项(根目录新建)
jest.setup.js 文件
require("jsdom-global")();
jest.config.js 文件
const path = require("path");
module.exports = {
rootDir: path.resolve(__dirname, "./"), // 根据自己项目来
moduleFileExtensions: ["js", "json", "vue"],
transformIgnorePatterns: ["/node_modules/"],
testEnvironment: "node",
collectCoverage: true, // 是否生成代码覆盖率报告,以及控制台是否打印覆盖率日志
collectCoverageFrom: ["**/src/components/**/*.{js,vue}"], // 设置要生成代码覆盖率报告的目录和文件
testRegex: "(/tests/.*\\.(test|spec))\\.[tj]sx?$", //测试文件的地址配置
transform: {
"^.+\\.js$": "<rootDir>/node_modules/babel-jest", //告诉 Jest 用 babel-jest 处理 JavaScript 测试文件
".*\\.(vue)$": "<rootDir>/node_modules/vue-jest" //告诉 Jest 用 vue-jest 处理 .vue 文件
},
moduleNameMapper: {
"^@/(.*)$": "<rootDir>/$1"
},
setupFiles: ["<rootDir>jest.setup.js"], //启动jest需要的文件
testEnvironmentOptions: {
url: "http://localhost"
}
};
笔记
Expect 断言
推荐文档:https://blog.csdn.net/biraotian/article/details/111329794
/* expect断言测试 */
const errorFn = () => {
throw new Error("错误信息!!");
};
describe("Expect断言测试", () => {
/* 常用 */
it("toBe():全等判断", () => {
expect(1).toBe(1);
});
it("not toBe:不等于", () => {
expect(2).not.toBe(1);
});
it("toEqual:匹配对象是否相等", () => {
expect({ username: "sunshine" }).toEqual({ username: "sunshine" });
});
it("toBeNull:匹配值是否为null", () => {
expect(null).toBeNull();
});
it("toBeUndefined:匹配是否为undefined", () => {
expect(undefined).toBeUndefined();
});
it("toBeTruthy:匹配某个值等于true", () => {
expect(true).toBeTruthy();
});
it("toBeFalsy:匹配某个值为假", () => {
expect(false).toBeFalsy();
});
/* 数字相关对比 */
it("toBeGreaterThan:比某个数字大", () => {
expect(1).toBeGreaterThan(0);
});
it("toBeLessThan:比某个数字小", () => {
expect(2).toBeLessThan(3);
});
it("toBeGreaterThanOrEqual:大于或等于某个数字", () => {
expect(2).toBeGreaterThanOrEqual(2);
});
it("toBeLessThanOrEqual:小于或等于某个数字", () => {
expect(1).toBeLessThanOrEqual(1);
});
it("toBeCloseTo:计算浮点数字相加是否相等", () => {
// expect(0.1 + 0.2).toBe(0.3); // js中0.1 + 0.2 = 0.30000000000000004
expect(0.1 + 0.2).toBeCloseTo(0.3);
});
/* 字符串相关 */
it("toMatch:字符串内是否包含某个字符串", () => {
expect("hello").toMatch("hel");
});
/* 数组相关 */
it("toContain:是否包含该元素", () => {
expect([1, 2, 3, 4]).toContain(3);
expect(new Set([1, 2, 3, 4])).toContain(3);
});
/* 异常相关 */
it("抛出错误", () => {
expect(errorFn).toThrow();
});
});
教程(@vue/test-utils)
包裹器 wrapper
属性
vm:vue 实例,可通过 wrapper.vm 访问该实例下的所有属性跟方法。
element:包裹器的根 Dom 节点
options:用于设置传入的数据、事件及注入依赖等
- propsData:设置组件属性数据
- slots:设置插槽内容
- listeners:设置自定义事件处理程序
- provide:注入依赖
- stubs:替换组件中的子组件
- mocks:提供模拟数据
- attrs:设置组件的 HTML 属性
selector:用于查找元素
wrapperTest.vue
<template>
<div>
<div>wrapper测试</div>
<h3>用户名:{{ username }}</h3>
<h3>角色:{{ role_name }}</h3>
</div>
</template>
<script>
export default {
props: ["username"],
data() {
return {
role_name: "-"
};
},
methods: {}
};
</script>
wrapper.test.js
import { shallowMount } from "@vue/test-utils";
import wrapperComponent from "@/src/views/wrapperTest.vue";
describe("wrapper属性", () => {
const wrapper = shallowMount(wrapperComponent, {
propsData: {
username: "sunshine"
}
});
// 获取vue实例
const vm = wrapper.vm;
// 获取元素节点
console.log(wrapper.element);
it("校验username并修改角色名", async () => {
// 校验username
expect(vm.username).toBe("sunshine");
// 修改角色名
await wrapper.setData({
role_name: "测试"
});
expect(vm.role_name).toBe("测试");
});
});

方法
attributes:用于将属性传递给组件的根元素
classes:返回节点的 class 数组
contains:判断是否包含了指定的元素或组件,下一版将废弃
destroy:销毁 vue 实例,一般在测试完毕后进行销毁
emitted:返回已经触发的事件记录
exists:断言 wrapper 或元素是否存在
find:查找 dom 节点或 wrapper(下一版本为 findComponent)
findAll:返回所有匹配的结果(下一版本为 findAllComponent)
html:返回 DOM 节点组成的字符串
get:跟 find 一样的效果,但是 get 未找到时会抛错(下一版本为 getComponent)
isVisible:判断组件是否可见,display:none; visibility:hidden 均为 false
props:获取组件的 props 属性
setChecked:设置 checkbox 或者 radio 元素的值,并改变 v-model 绑定的数据
setData:用于设置 data 中的数据
setProps:用于设置 props
setSelected:选中一个 options 选项,并改变 v-model 的值
setValue:设置一个文本控件或 select 的值并更新 v-model
text:返回文本内容
trigger:触发 DOM 事件
Foo.vue
<template>
<div>
<div class="title">{{ title }}</div>
<div class="secondTitle">二级标题</div>
<!-- 接受attributes -->
<div id="attributes" class="my-component" v-bind="$attrs">
<slot></slot>
</div>
<!-- isVisible -->
<button v-if="showButton" @click="hiddenButton">确定</button>
<!-- checkbox -->
<input type="checkbox" v-model="isChecked" />
<!-- options -->
<select>
<option value="volvo">Volvo</option>
<option value="saab">Saab</option>
<option value="opel">Opel</option>
<option value="audi">Audi</option>
</select>
<!-- input -->
<input type="text" v-model="text" />
</div>
</template>
<script>
export default {
props: ["title", "secondTitle"],
data() {
return {
showButton: true,
text: ""
};
},
methods: {
setData(val) {
return val;
},
hiddenButton() {
this.showButton = false;
}
}
};
</script>
wrapperMethods.test.js
import { mount } from "@vue/test-utils";
import Foo from "@/src/views/wrapper/Foo.vue";
describe("测试wrapper的方法", () => {
it("attributes:用于将属性传递给组件的根元素。它可以用于传递除了组件的props之外的任何HTML属性", () => {
const attributes = {
class: "my-class",
customAttribute: "custom-value"
};
const wrapper = mount(Foo, {
attrs: attributes
});
console.log(wrapper.attributes());
let div = wrapper.find("div#attributes");
console.log(div.attributes());
console.log(div.attributes("id"));
console.log(div.attributes().id);
expect(div.attributes().id).toBe("attributes");
});
it("classes:返回dom元素的clas数组", () => {
const wrapper = mount(Foo);
let classes = wrapper.find("div.title").classes();
console.log(classes); // Array
expect(classes.includes("title"));
});
it("contains:判断是否包含了指定的元素或组件", () => {
const wrapper = mount(Foo);
expect(wrapper.contains("div")).toBe(true);
});
it("destroy:销毁vue实例", () => {
const wrapper = mount(Foo);
/* 一些操作后 */
wrapper.destroy();
});
it("emitted:记录已经触发的事件", async () => {
const wrapper = mount(Foo);
await wrapper.vm.$emit("setData");
await wrapper.vm.$emit("setData", 123);
// 判断事件是否被触发
expect(wrapper.emitted().setData).toBeTruthy();
// 触发的数量
expect(wrapper.emitted().setData.length).toBe(2);
// 校验传递过去的数据
expect(wrapper.emitted().setData[1]).toEqual([123]);
});
it("exists:检查包裹器或元素是否存在", () => {
const wrapper = mount(Foo);
expect(wrapper.exists()).toBe(true);
// 检查是否存在button
expect(wrapper.find("button").exists()).toBe(true);
// 检查是否存在class为title的div
expect(wrapper.find("div.title").exists()).toBe(true);
});
it("html:返回dom生成的字符串", () => {
const wrapper = mount(Foo, {
propsData: {
title: "测试标题"
}
});
console.log(wrapper.findComponent("div.title").html());
expect(wrapper.findComponent("div.title").html()).toBe(
'<div class="title">测试标题</div>'
);
});
it("isVisible:判断组件是否可见", () => {
const wrapper = mount(Foo);
const vm = wrapper.vm;
// 校验原始值
expect(vm.showButton).toBe(true);
// 校验button是否可见
expect(wrapper.find("button").isVisible()).toBe(true);
});
it("props:访问组件的props", () => {
const wrapper = mount(Foo, {
propsData: {
title: "props测试标题",
secondTitle: "二级标题"
}
});
const props = wrapper.props();
// 校验title
expect(props.title).toBe("props测试标题");
// 校验secondTitle
expect(props.secondTitle).toBe("二级标题");
});
it("setChecked", async () => {
const wrapper = mount(Foo);
const checkboxInput = wrapper.find('input[type="checkbox"]');
console.log(checkboxInput.exists());
await checkboxInput.setChecked(true);
// 判断是否被选中
expect(checkboxInput.element.checked).toBe(true);
});
it("setData:设置data的数据", async () => {
const wrapper = mount(Foo);
await wrapper.setData({
showButton: false
});
// 校验button是否隐藏
expect(wrapper.find("button").exists()).toBe(false);
});
it("setProps:用于设置props的数据", async () => {
const wrapper = mount(Foo);
await wrapper.setProps({
title: "setProps测试"
});
expect(wrapper.find("div.title").text()).toBe("setProps测试");
});
it("setSelected", async () => {
const wrapper = mount(Foo);
const options = wrapper.find("select").findAll("option");
await options.at(1).setSelected();
// 验证是否被选中
expect(wrapper.find("option:checked").element.value).toBe("saab");
});
it("setValue:设置一个文本的值或select的值", async () => {
const wrapper = mount(Foo);
const input = wrapper.find('input[type="text"]');
await input.setValue("测试setValue");
console.log(wrapper.vm.text);
expect(wrapper.vm.text).toBe("测试setValue");
});
});
挂载组件
@vue/test/utils 会模拟必要的输入(props、注入跟用户事件)以及对输出(渲染结果、触发的自定义事件)的断言来测试 Vue 组件。被挂在的组件会返回一个包裹器,包裹器会暴露很多封装、遍历和查询其内部的 Vue 组件实例的方法。 通过 mount 方法来创建包裹器:
mount 方法创建包裹器
counter.vue
<template>
<div>
<span class="count">{{ count }}</span>
<button @click="increment">Increment</button>
</div>
</template>
<script>
export default {
data() {
return {
count: 0
};
},
methods: {
increment() {
this.count++;
}
}
};
</script>
counter.test.js
import Vue from "vue";
import { mount } from "@vue/test-utils";
import CounterComponent from "@/src/views/counter.vue";
describe("测试计数组件", () => {
// 创建包裹器
const wrapper = mount(CounterComponent);
// 通过wrapper.vm访问Vue实例
const vm = wrapper.vm;
it("打印页面结构", () => {
console.log(wrapper.html());
expect(wrapper.html()).toContain('<span class="count">0</span>');
});
it("检查元素是否存在", () => {
expect(wrapper.find("button").exists()).toBe(true);
});
/* 模拟用户交互 */
it("模拟用户点击", () => {
// 先判断当前值
expect(vm.count).toBe(0);
// 获取button元素
const button = wrapper.find("button");
button.trigger("click"); // 模拟点击
// 校验当前值
expect(vm.count).toBe(1);
});
/* 异步操作 */
it("异步操作", async () => {
expect(wrapper.text()).toContain("1");
const button = wrapper.find("button");
await button.trigger("click");
expect(wrapper.text()).toContain("2");
});
// 注意:使用nextTick时,其内部的错误可能不会被捕获,因为内部使用了promise
it("该用例会超时", done => {
// "Exceeded timeout of 5000 ms for a test while waiting for `done()` to be called.
Vue.nextTick(() => {
expect(true).toBe(false); // 测试超时
done();
});
});
// 接下来的三项测试都会如预期工作
it("will catch the error using done", done => {
// 方法一:将Vue全局错误处理器设置为done
Vue.config.errorHandler = done;
Vue.nextTick(() => {
expect(true).toBe(false); // 测试不通过
done();
});
});
it("will catch the error using a promise", () => {
// 方法二:将其作为返回
return Vue.nextTick().then(function () {
expect(true).toBe(false); // 测试不通过
});
});
it("will catch the error using async/await", async () => {
// 方法三:使用async await
await Vue.nextTick();
expect(true).toBe(false); // 测试不通过
});
it("异步链式结构:有错误识别不出来", () => {
const button = wrapper.find("button");
button.trigger("click").then(() => {
expect(vm.count).toBe(66);
button.trigger("click").then(() => {
expect(vm.count).toBe(2);
});
});
});
});
shallowMount 浅渲染
浅渲染,只加载当前组件,而不加载其子组件,其余用法与 mount 一致
import { shallowMount } from "@vue/test-utils";
import CounterComponent from "@/src/views/counter.vue";
describe("浅渲染", () => {
const wrapper = shallowMount(CounterComponent);
const vm = wrapper.vm;
it("浅渲染内容获取", () => {
expect(vm.count).toBe(2); // 测试不通过,真实值为0
console.log(vm);
});
});
挂载选项(options)
data:会并入现有的 data 中
const Component = {
template: `
<div>
<span id="foo">{{ foo }}</span>
<span id="bar">{{ bar }}</span>
</div>
`,
data() {
return {
foo: "foo",
bar: "bar"
};
}
};
const wrapper = mount(Component, {
data() {
return {
bar: "my-override"
};
}
});
wrapper.find("#foo").text(); // 'foo'
wrapper.find("#bar").text(); // 'my-override'
slots:为组件提供一个 slot 内容的对象。该对象中的键名就是相应的 slot 名,键值可以是一个组件、一个组件数组、一个字符串模板或文本。
import Foo from "./Foo.vue";
import MyComponent from "./MyComponent.vue";
const bazComponent = {
name: "baz-component",
template: "<p>baz</p>"
};
const yourComponent = {
props: {
foo: {
type: String,
required: true
}
},
render(h) {
return h("p", this.foo);
}
};
const wrapper = shallowMount(Component, {
slots: {
default: [Foo, "<my-component />", "text"],
fooBar: Foo, // 将会匹配 `<slot name="FooBar" />`.
foo: "<div />",
bar: "bar",
baz: bazComponent,
qux: "<my-component />",
quux: '<your-component foo="lorem"/><your-component :foo="yourProperty"/>'
},
stubs: {
// 用来注册自定义组件
"my-component": MyComponent,
"your-component": yourComponent
},
mocks: {
// 用来向渲染上下文添加 property
yourProperty: "ipsum"
}
});
expect(wrapper.find("div")).toBe(true);
mocks:传入 mock 数据
const $route = { path: "http://www.example-path.com" };
const wrapper = shallowMount(Component, {
mocks: {
$route
}
});
expect(wrapper.vm.$route.path).toBe($route.path);
localVue:通过 createLocalVue 创建的一个 Vue 的本地拷贝,用于挂载该组件的时候。在这份拷贝上安装插件可以防止原始的 Vue 被污染
import { createLocalVue, mount } from "@vue/test-utils";
import VueRouter from "vue-router";
import Foo from "./Foo.vue";
const localVue = createLocalVue();
localVue.use(VueRouter);
const routes = [{ path: "/foo", component: Foo }];
const router = new VueRouter({
routes
});
const wrapper = mount(Component, {
localVue,
router
});
expect(wrapper.vm.$route).toBeInstanceOf(Object);
attachTo:指定一个 HTMLElement 或定位到一个 HTML 元素的 CSS 选择器字符串,组件将会被完全挂载到文档中的这个元素。
const Component = {
template: "<div>ABC</div>"
};
let wrapper = mount(Component, {
attachTo: "#root"
});
expect(wrapper.vm.$el.parentNode).to.not.be.null;
wrapper.destroy();
wrapper = mount(Component, {
attachTo: document.getElementById("root")
});
expect(wrapper.vm.$el.parentNode).to.not.be.null;
wrapper.destroy();
attrs:设置组件实例的 $attrs 对象
propsData:设置 props 数据
listeners:设置组件实例的 $listeners 对象。
const Component = {
template: "<button v-on:click=\"$emit('click')\"></button>"
};
const onClick = jest.fn();
const wrapper = mount(Component, {
listeners: {
click: onClick
}
});
wrapper.trigger("click");
expect(onClick).toHaveBeenCalled();
parentComponent:用来作为被挂载组件的父级组件
import Foo from "./Foo.vue";
const wrapper = shallowMount(Component, {
parentComponent: Foo
});
expect(wrapper.vm.$parent.$options.name).toBe("foo");
provide:为组件传递用于注入的属性。
const Component = {
inject: ["foo"],
template: "<div>{{this.foo()}}</div>"
};
const wrapper = shallowMount(Component, {
provide: {
foo() {
return "fooValue";
}
}
});
expect(wrapper.text()).toBe("fooValue");
选择器
CSS 选择器
- 标签选择器
- id 选择器
- 类选择器
- 特性选择器:div.className
查找选项对象
使用 wrapper.find 进行查找
const button = wrapper.find("button.isChecked"); // 查找class为isChecked的button
button.trigger("click");
生命周期钩子
在使用 mount 或 shallowMount 时,可以期望该组件响应 Vue 所有生命周期事件。但是 beforeDestroy 和 destroy 必须使用**wrapper.destroy()**才会触发。此外每个测试结束时并不会自动销毁,而是用户决定是否清理在结束前继续运行的任务,例如 setInterval 或 setTimeout.
beforeAll:在所有测试用例执行前运行
beforeEach:在每个测试用例执行前执行一次
afterEach:在每个测试用例执行后执行一次
afterAll:在所有测试用例结束后运行
Mocks 数据及 props
仿造 prop
使用内置 propsData 选项向组件传入 prop
propsView.vue
<template>
<div>
props测试
<h1>当前用户名:{{ username }}</h1>
<h1>角色:{{ role_name }}</h1>
</div>
</template>
<script>
export default {
props: ["username", "role_name"],
data() {
return {};
}
};
</script>
props.test.js
/* props传入测试 */
import { shallowMount } from "@vue/test-utils";
import propsView from "@/src/views/propsView.vue";
describe("测试props传入", () => {
const wrapper = shallowMount(propsView, {
propsData: {
username: "sunshine",
role_name: "超级管理员"
}
});
const vm = wrapper.vm;
it("验证传入的数据", () => {
console.log(vm.username); // sunshine
console.log(vm.role_name); // 超级管理员
expect(vm.username).toBe("sunshine");
expect(vm.role_name).toBe("超级管理员");
});
});
操作组件状态
用 setData 或 setProps 方法直接操作组件状态
props.test.js
/* props传入测试 */
import { shallowMount } from "@vue/test-utils";
import propsView from "@/src/views/propsView.vue";
describe("测试props传入", () => {
const wrapper = shallowMount(propsView, {
propsData: {
username: "sunshine",
role_name: "超级管理员"
}
});
const vm = wrapper.vm;
it("验证传入的数据", () => {
console.log(vm.username);
console.log(vm.role_name);
expect(vm.username).toBe("sunshine");
expect(vm.role_name).toBe("超级管理员");
});
it("操作组件状态", async () => {
await wrapper.setProps({
username: "Jone"
});
console.log(vm.username); // Jone
expect(vm.username).toBe("Jone");
});
});
仿造注入
使用 mocks 选项注入 props 或$route
mocksView.vue
<template>
<div>
mocks选项
<div class="infos">
{{ $route.params.username }}
</div>
</div>
</template>
<script>
export default {
props: [],
data() {
return {};
}
};
</script>
mocks.test.js
import { shallowMount } from "@vue/test-utils";
import mocksView from "@/src/views/mocksView.vue";
describe("测试mocks注入", () => {
it("注入$route", () => {
const $route = {
path: "/",
hash: "",
params: {
username: "Jone"
},
query: {
role_name: "管理员"
}
};
const wrapper = shallowMount(mocksView, {
mocks: {
$route
}
});
// 查找class为info的元素,并获取其中内容
expect(wrapper.find(".infos").text()).toBe("Jone");
});
});
键盘、鼠标事件
触发事件
wrapper 暴露了一个 trigger 方法,可用于触发 Dom 事件,且 trigger 可接受选项,即传参
triggerTest.vue
<template>
<div>
trigger事件测试
<div class="count">当前计数:{{ count }}</div>
<button class="increment" @click="increment">调用加法函数</button>
<button class="subtraction" @click="subtraction">调用减法函数</button>
<button class="options" @click="options">调用传参函数</button>
<div class="infos">{{ infos }}</div>
</div>
</template>
<script>
export default {
data() {
return {
count: 0,
infos: null
};
},
methods: {
increment() {
this.count++;
},
subtraction() {
this.count--;
},
options(val) {
this.infos = val;
}
}
};
</script>
trigger.test.js
/* 方法测试 */
import { shallowMount } from "@vue/test-utils";
import triggerTest from "@/src/views/triggerTest.vue";
describe("方法测试", () => {
const wrapper = shallowMount(triggerTest);
const vm = wrapper.vm;
it("测试调用加法函数", async () => {
const button = wrapper.find(".increment");
await button.trigger("click");
console.log(vm.count);
expect(vm.count).toBe(1);
});
it("接受传参", async () => {
const button = wrapper.find(".options");
await button.trigger("click", {
username: "Jone"
});
console.log(vm.infos);
expect(vm.infos.username).toBe("Jone");
});
});
断言触发的事件
每个挂载的包裹器都会通过其背后的 Vue 实例自动记录所有被触发的事件。你可以用 wrapper.emitted() 方法取回这些事件记录.
/* 回收触发的记录 */
it("回收触发的记录", () => {
wrapper.vm.$emit("foo");
wrapper.vm.$emit("foo", 123);
console.log(wrapper.emitted()); // { foo: [ [], [ 123 ] ] }
});
从子组件触发事件
father.vue
<template>
<div>
父组件
<p v-if="emitted">Emitted!</p>
<children-component @custom="onCustom"></children-component>
</div>
</template>
<script>
import ChildrenComponent from "./children.vue";
export default {
components: {
ChildrenComponent
},
data() {
return {
emitted: false
};
},
methods: {
onCustom() {
this.emitted = true;
}
}
};
</script>
children.vue
<template>
<div>
子组件
<button @click="emitClick">调用父组件</button>
</div>
</template>
<script>
export default {
data() {
return {};
},
methods: {
emitClick() {
this.$emit("custom");
}
}
};
</script>
childrenTest.test.js
/* 子组件调用父组件方法测试 */
import { mount } from "@vue/test-utils";
import FatherComponent from "@/src/views/TranslateTest/father.vue";
import ChildrenComponent from "@/src/views/TranslateTest/children.vue";
describe("子组件调用父组件", () => {
const father_wrapper = mount(FatherComponent);
const fa_vm = father_wrapper.vm;
it("子组件中调用", async () => {
father_wrapper.findComponent(ChildrenComponent).vm.$emit("custom");
expect(fa_vm.emitted).toBe(true); // 测试通过
});
});
Demo:键盘实例
KeyboardClickDemo.vue
<template>
<input type="text" @keydown.prevent="onKeydown" v-model="quantity" />
</template>
<script>
const KEY_DOWN = 40;
const KEY_UP = 38;
const ESCAPE = 27;
export default {
data() {
return {
quantity: 0
};
},
methods: {
increment() {
this.quantity += 1;
},
decrement() {
this.quantity -= 1;
},
clear() {
this.quantity = 0;
},
onKeydown(e) {
if (e.keyCode === ESCAPE) {
this.clear();
}
if (e.keyCode === KEY_DOWN) {
this.decrement();
}
if (e.keyCode === KEY_UP) {
this.increment();
}
if (e.key === "a") {
this.quantity = 13;
}
}
},
watch: {
quantity: function (newValue) {
this.$emit("input", newValue);
}
}
};
</script>
keyboard.test.js
import { mount } from "@vue/test-utils";
import KeyboardClickDemo from "@/src/views/KeyboardClickDemo.vue";
describe("键盘事件测试", () => {
it("验证默认值", () => {
const wrapper = mount(KeyboardClickDemo);
expect(wrapper.vm.quantity).toBe(0);
});
it("向上箭头+1", async () => {
const wrapper = mount(KeyboardClickDemo);
await wrapper.trigger("keydown.up");
expect(wrapper.vm.quantity).toBe(1);
});
it("向下箭头-1", async () => {
const wrapper = mount(KeyboardClickDemo);
wrapper.vm.quantity = 5;
await wrapper.trigger("keydown.down");
expect(wrapper.vm.quantity).toBe(4);
});
it("esc清空", async () => {
const wrapper = mount(KeyboardClickDemo);
wrapper.vm.quantity = 5;
await wrapper.trigger("keydown.esc");
expect(wrapper.vm.quantity).toBe(0);
});
it("小写a将值改为13", async () => {
const wrapper = mount(KeyboardClickDemo);
await wrapper.trigger("keydown", {
key: "a"
});
expect(wrapper.vm.quantity).toBe(13);
});
});
Demos
ToFixed 函数测试
/plugins/index.js 文件
export default {
/**
* 格式化数字、字符串,小数位数默认2位
* 例:电压精确小数点3位
* 0 -> 0
* 0.00 -> 0
* 12 -> 12
* 12.01 -> 12.01
* 12.145 -> 12.145
* 12.1456 -> 12.146
* 12.1451 -> 12.145
* 12.1599 -> 12.16
* 12.1999 -> 12.2
* 12.9999 -> 13
* -0.0012 -> 0
* -0.00000001 -> 0
*/
toFixed(val, length = 2) {
//不是数字,返回-
if (isNaN(parseFloat(val))) {
return "-";
}
if (["number", "string"].includes(typeof val)) {
if (val == 0) return 0;
if (String(val).match(/\./) || String(val).includes("e-")) {
let newVal = parseFloat(Number(val).toFixed(length));
if (newVal === 0) return 0; //转换后有可能是 -0
return newVal;
} else {
return val;
}
}
return "-";
}
};
/tests/toFixed.test.js 文件
/** 针对toFixed函数的单元测试
*/
const { toFixed } = require("@/src/plugins/index.js").default;
describe("测试toFixed方法", () => {
// 主描述
it("0测试", () => {
// 测试用例1
expect(toFixed(0)).toBe(0);
});
it("0.00测试", () => {
// 测试用例2
expect(toFixed(0.0)).toBe(0);
});
it("12测试", () => {
expect(toFixed(12)).toBe(12);
});
it("12.01测试", () => {
expect(toFixed(12.01)).toBe(12.01);
});
it("12.456测试", () => {
expect(toFixed(12.456)).toBe(12.46);
});
});
结果
匹配结果

模拟接口测试
Login.vue 文件
<template>
<div>用户登录</div>
</template>
<script>
import axios from "axios";
export default {
data() {
return {
username: "-"
};
},
methods: {
login({ username, password }) {
axios
.post("/login", { username, password })
.then(res => {
this.username = res.data.username ?? "-";
})
.catch(err => {
console.log(err);
throw err;
});
}
}
};
</script>
login.test.js 文件
/* 用户登录接口测试 */
import { shallowMount } from "@vue/test-utils";
import loginComponent from "@/src/views/Login.vue";
// 模拟axios函数(注意:此方法会临时替换axios模块)
jest.mock("axios", () => ({
post: jest.fn(() =>
Promise.resolve({
data: {
username: "sunshine",
password: "a123456"
}
})
)
}));
describe("测试用户登录", () => {
const loginData = {
username: "sunshine",
password: "a123456"
};
const wrapper = shallowMount(loginComponent); // 组件挂载
it("测试登录", async () => {
await wrapper.vm.login(loginData);
expect(wrapper.vm.username).toBe("sunshine");
});
});
结果
不匹配时

匹配时

文档
官方文档:https://jestjs.io/zh-Hans/docs/expect
vue-test-utils 文档:https://v1.test-utils.vuejs.org/zh/guides/#起步
博客:https://blog.csdn.net/lambert00001/article/details/130912370
Loading...
