Jest

时游大约 15 分钟日常学习笔记前端单元测试

前端单元测试

插件安装

推荐安装 Jest 、JestRunner、Jest Snippets 插件(vscode)

image-20230830145153501
image-20230830145153501

安装

注意:要将 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/111329794open in new window

/* 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:用于设置传入的数据、事件及注入依赖等
  1. propsData:设置组件属性数据
  2. slots:设置插槽内容
  3. listeners:设置自定义事件处理程序
  4. provide:注入依赖
  5. stubs:替换组件中的子组件
  6. mocks:提供模拟数据
  7. 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("测试");
	});
});
image-20230904172749474
image-20230904172749474

方法

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:通过 createLocalVueopen in new window 创建的一个 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 选择器
  1. 标签选择器
  2. id 选择器
  3. 类选择器
  4. 特性选择器: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("超级管理员");
	});
});
操作组件状态

​ 用 setDatasetProps 方法直接操作组件状态

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);
	});
});

结果

匹配结果
image-20230901104434099
image-20230901104434099

模拟接口测试

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");
	});
});

结果

不匹配时
image-20230831163309372
image-20230831163309372
匹配时
image-20230831163330050
image-20230831163330050

文档

官方文档:https://jestjs.io/zh-Hans/docs/expectopen in new window

vue-test-utils 文档:https://v1.test-utils.vuejs.org/zh/guides/#起步open in new window

博客:https://blog.csdn.net/lambert00001/article/details/130912370open in new window

上次编辑于:
贡献者: 15327360835,minmengwei
Loading...