浅谈Vue组件单元测试究竟测试什么
关于 Vue 组件单元测试最常见的问题就是“我究竟应该测试什么?”
虽然测试过多或过少都是可能的,但我的观察是,开发人员通常会测试过头。毕竟,没有人愿意自己的组件未经测试从而导致应用程序在生产中崩溃。
在本文中,我将分享一些用于组件单元测试的指导原则,这些指导原则可以确保在编写测试上不会花费大量时间,但是可以提供足够的覆盖率来避免错误。
本文假设你已经了解 Jest 和 Vue Test Utils。
示例组件
在学习这些指导原则之前,我们先来熟悉下要测试的示例组件。组件名为 Item.vue ,是 eCommerce App 里的一个产品条目。
下面是组件的源码。注意有三个依赖项:Vuex ( $store
), Vue Router ( $router
) 和 Vue Auth ( $auth
)。
Item.vue
<template> <div> <h2>{{ item.title }}</h2> <button @click="addToCart">Add To Cart</button> <img :src="item.image"/> </div> </template> <script> export default { name: "Item", props: [ "id" ], computed: { item () { return this.$store.state.find( item => item.id === this.id ); } }, methods: { addToCart () { if (this.$auth.check()) { this.$store.commit("ADD_TO_CART", this.id); } else { this.$router.push({ name: "login" }); } } } }; </script>
配置 Spec 文件
下面是测试用的 spec 文件。其中,我们将用 Vue Test Utils “浅挂载”示例组件,因此引入了相关模块以及我们要测试的 Item 组件。
同时还写了一个工厂函数用于生成可覆盖的配置对象,以免在每个测试中都需要指定 props 和 mock 三个依赖项。 item.spec.js
import { shallowMount } from "@vue/test-utils"; import Item from "@/components/Item"; function createConfig (overrides) { const id = 1; const mocks = { // Vue Auth $auth: { check: () => false }, // Vue Router $router: { push: () => {} }, // Vuex $store: { state: [ { id } ], commit: () => {} } }; const propsData = { id }; return Object.assign({ mocks, propsData }, overrides); } describe("Item.vue", () => { // Tests go here });
确定业务逻辑
对于要测试的组件,要问的第一个也是最重要的问题是“业务逻辑是什么”,即组件是做什么的?
对于这个 Item.vue ,业务逻辑是:
- 根据接收的id属性展示条目信息
- 如果用户是访客,点击 Add to Cart 按钮将重定向到登录页
- 如果用户已登录,点击 Add to Cart 按钮会触发 Vuex mutation ADD_TO_CART。
确定输入和输出
当你对组件做单元测试时,可将其视为一个黑盒。方法、计算属性等内部逻辑只影响输出。
因此,下一个重点是确定组件的输入和输出,因为这些也是测试的输入和输出。
Item.vue 的输入是:
- id 属性
- 来自 Vuex 和 Vue Auth 的数据状态
- 用户点击按钮
输出是:
- 渲染后的 HTML
- 发送到 Vuex mutation 或者 Vue Router push 的数据
有些组件也会将表单和事件作为输入,触发事件作为输出。
测试 1: 访客点击按钮跳转路由
有一个业务逻辑是“如果用户是访客,点击 Add to Cart 按钮将重定向到登录页”。我们来写这个测试。
我们通过“shallow mount”组件来编写测试,然后找到并点击 Add to Cart 按钮。
test("router called when guest clicks button", () => { const config = createConfig(); const wrapper = shallowMount(Item, config); wrapper .find("button") .trigger("click"); // Assertion goes here }
随后我们会加上 assertion。
不要超出输入和输出的界限
在这个测试中很容易采取的做法是在点击按钮后判断路由是否跳转到了登录页,比如:
import router from "router"; test("router called when guest clicks button", () => { ... // 错! const route = router.find(route => route.name === "login"); expect(wrapper.vm.$route.path).toBe(route.path); }
虽然这确实也能测试组件的输出,但是它依赖于路由功能,这不应该是组件所关心的。
直接测试组件的输出会更好,也就是调用了 $router.push
。至于路由是否最终完成了操作,这已经超出了本测试的范畴。
因此我们可以监听路由的 push
方法,并断言它是否被登录路由对象调用。
import router from "router"; test("router called when guest clicks button", () => { ... jest.spyOn(config.mocks.$router, "push"); const route = router.find(route => route.name === "login"); expect(spy).toHaveBeenCalledWith(route); }
测试 2: 登录用户点击按钮后调用 vuex
接下来让我们测试业务逻辑“如果用户已登录,点击 Add to Cart 按钮将触发 Vuex mutation ADD_TO_CART
”。
同样,你不需要判断 Vuex 状态是否更改了。要验证这个需要另外单独测试 Vuex store。
组件的职责只是执行 commit,因此我们只要测试这个动作就行。
首先重写 $auth.check
假数据让它返回 true
(模拟登录用户)。然后监听 store 的 commit
方法,并断言点击按钮后被调用。
test("vuex called when auth user clicks button", () => { const config = createConfig({ mocks: { $auth: { check: () => true } } }); const spy = jest.spyOn(config.mocks.$store, "commit"); const wrapper = shallowMount(Item, config); wrapper .find("button") .trigger("click"); expect(spy).toHaveBeenCalled(); }
不要测试其他库的功能
Item 组件展示条目数据,特别是标题和图片。或许我们应该写一个测试来专门检查这些?比如:
test("renders correctly", () => { const wrapper = shallowMount(Item, createConfig()); // Wrong expect(wrapper.find("h2").text()).toBe(item.title); }
这又是一个不必要的测试,因为它只是测试了 Vue 从 Vuex 中提取数据并插入到模板的能力。Vue 这个库已经对该机制进行了测试,所以你应该依赖于它。
测试 3: 正确地渲染
但是等等,如果有人不小心将 title
重命名为 name
,然后忘记更新插值表达式怎么办?这难道不需要测试吗?
没错,但是如果你像这样来测试模板的方方面面,何时才是个头?
测试 HTML 最好的办法是使用快照,用来检查整体渲染后的结果。这不仅覆盖了标题插值,还包括图片、按钮文本、任何 class 等。
test("renders correctly", () => { const wrapper = shallowMount(Item, createConfig()); expect(wrapper).toMatchSnapshot(); });
其他不需要测试的点还有这些:
- src 属性是否绑定到 img 元素
- 添加到 Vuex store 中的数据是否跟插入的数据一致
- 计算属性是否返回了正确的数据
- 执行 router push 是否重定向到正确的页面
诸如此类。
总结
我认为上面三个简单的测试对这个组件来说足够了。
组件单元测试的一个好理念是先假设测试是不必要的,除非被证明是必要的。
你可以问自己以下问题:
- 这是业务逻辑的一部分吗?
- 这是直接测试组件的输入和输出吗?
- 这是测试自己的代码,还是第三方代码?