挂载组件与选项
大约 4 分钟
挂载组件与选项
@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:在所有测试用例结束后运行
Loading...
