浅谈Vue组件单元测试究竟测试什么


Posted in Javascript onFebruary 05, 2020

关于 Vue 组件单元测试最常见的问题就是“我究竟应该测试什么?”

虽然测试过多或过少都是可能的,但我的观察是,开发人员通常会测试过头。毕竟,没有人愿意自己的组件未经测试从而导致应用程序在生产中崩溃。

在本文中,我将分享一些用于组件单元测试的指导原则,这些指导原则可以确保在编写测试上不会花费大量时间,但是可以提供足够的覆盖率来避免错误。

本文假设你已经了解 Jest 和 Vue Test Utils。

示例组件

在学习这些指导原则之前,我们先来熟悉下要测试的示例组件。组件名为 Item.vue ,是 eCommerce App 里的一个产品条目。

浅谈Vue组件单元测试究竟测试什么

下面是组件的源码。注意有三个依赖项: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 是否重定向到正确的页面

诸如此类。

总结

我认为上面三个简单的测试对这个组件来说足够了。
组件单元测试的一个好理念是先假设测试是不必要的,除非被证明是必要的。

你可以问自己以下问题:

  • 这是业务逻辑的一部分吗?
  • 这是直接测试组件的输入和输出吗?
  • 这是测试自己的代码,还是第三方代码?

让我们愉快地单元测试吧!希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
jquery 可拖拽的窗体控件实现代码
Mar 21 Javascript
jquery中map函数与each函数的区别实例介绍
Jun 23 Javascript
最流行的Node.js精简型和全栈型开发框架介绍
Feb 26 Javascript
javascript实现的多个层切换效果通用函数实例
Jul 06 Javascript
javascript中undefined与null的区别
Aug 16 Javascript
JS+CSS实现的竖向简洁折叠菜单效果代码
Oct 22 Javascript
详解JavaScript的回调函数
Nov 20 Javascript
node.js学习之交互式解释器REPL详解
Dec 08 Javascript
vue上传图片组件编写代码
Jul 26 Javascript
详解webpack 打包文件体积过大解决方案(code splitting)
Apr 10 Javascript
JavaScript封装的常用工具类库bee.js用法详解【经典类库】
Sep 03 Javascript
微信小程序页面上下滚动效果
Nov 18 Javascript
VUE中使用HTTP库Axios方法详解
Feb 05 #Javascript
Vue获取页面元素的相对位置的方法示例
Feb 05 #Javascript
vue.js使用v-model实现父子组件间的双向通信示例
Feb 05 #Javascript
vue使用原生swiper代码实例
Feb 05 #Javascript
Vue如何使用混合Mixins和插件开发详解
Feb 05 #Javascript
JS原型和原型链原理与用法实例详解
Feb 05 #Javascript
js实现视图和数据双向绑定的方法分析
Feb 05 #Javascript
You might like
PHP中获取内网用户MAC地址(WINDOWS/linux)的实现代码
2011/08/11 PHP
PHP字符串的递增和递减示例介绍
2014/02/11 PHP
PHP编写的图片验证码类文件分享
2016/06/06 PHP
php+jQuery递归调用POST循环请求示例
2016/10/14 PHP
详解php中 === 的使用
2016/10/24 PHP
基于php流程控制语句和循环控制语句(讲解)
2017/10/23 PHP
基于php中echo用逗号和用点号的区别详解
2018/01/23 PHP
js no-repeat写法 背景不重复
2009/03/18 Javascript
IE8 下的Js错误HTML Parsing Error...
2009/08/14 Javascript
JS构建页面的DOM节点结构的实现代码
2011/12/09 Javascript
jquery方法+js一般方法+js面向对象方法实现拖拽效果
2012/08/30 Javascript
javascript中的缓动效果实现程序
2012/12/29 Javascript
使用jQuery validate 验证注册表单实例演示
2013/03/25 Javascript
为jquery的ajaxfileupload增加附加参数的方法
2014/03/04 Javascript
javascript中加号(+)操作符的一些神奇作用
2014/06/06 Javascript
利用JS实现点击按钮后图片自动切换的简单方法
2016/10/24 Javascript
vue 2.0组件与v-model详解
2017/03/27 Javascript
关于angular js_$watch监控属性和对象详解
2017/04/24 Javascript
vue、react等单页面项目部署到服务器的方法及vue和react的区别
2018/09/29 Javascript
nodejs更新package.json中的dependencies依赖到最新版本的方法
2018/10/10 NodeJs
Python实现模拟时钟代码推荐
2015/11/08 Python
Win10下Python环境搭建与配置教程
2016/11/18 Python
Python2包含中文报错的解决方法
2018/07/09 Python
python和opencv实现抠图
2018/07/18 Python
python入门:这篇文章带你直接学会python
2018/09/14 Python
python通过http下载文件的方法详解
2019/07/26 Python
django admin 根据choice字段选择的不同来显示不同的页面方式
2020/05/13 Python
CSS3近阶段篇之酷炫的3D旋转透视
2016/04/28 HTML / CSS
CSS Houdini实现动态波浪纹效果
2019/07/30 HTML / CSS
请用用Java代码写一个堆栈
2012/01/26 面试题
机电专业体育教师求职信
2013/09/21 职场文书
最热门的自我评价
2013/12/30 职场文书
入党积极分子评语
2014/05/04 职场文书
个人作风纪律整顿整改措施
2014/10/25 职场文书
服务员岗位职责
2015/02/03 职场文书
逃出克隆岛观后感
2015/06/09 职场文书