浅谈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 相关文章推荐
求解开jscript.encode代码的asp函数
Feb 28 Javascript
Javascript select控件操作大全(新增、修改、删除、选中、清空、判断存在等)
Dec 19 Javascript
js判断浏览器是否支持html5
Aug 17 Javascript
JavaScript实现的encode64加密算法实例分析
Apr 15 Javascript
详解JavaScript中的Unescape()和String() 函数
Nov 09 Javascript
原生js三级联动的简单实现代码
Jun 07 Javascript
JS实现移动端判断上拉和下滑功能
Aug 07 Javascript
使用vue实现简单键盘的示例(支持移动端和pc端)
Dec 25 Javascript
React Router v4 入坑指南(小结)
Apr 08 Javascript
vue使用Element组件时v-for循环里的表单项验证方法
Jun 28 Javascript
微信小程序搜索功能(附:小程序前端+PHP后端)
Feb 28 Javascript
微信小程序APP的生命周期及页面的生命周期
Apr 19 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中几种常见的超时处理全面总结
2012/09/11 PHP
基于php验证码函数的使用示例
2013/05/03 PHP
php中的mongodb select常用操作代码示例
2014/09/06 PHP
PHP数组生成XML格式数据的封装类实例
2016/11/10 PHP
js prototype截取字符串函数
2010/04/01 Javascript
JS的反射问题
2010/04/07 Javascript
Extjs单独定义各组件的实例代码
2013/06/25 Javascript
js验证电话号码与手机支持+86的正则表达式
2014/01/23 Javascript
js验证IP及子网掩码的合法性有效性示例
2014/04/30 Javascript
javascript 动态修改css样式方法汇总(四种方法)
2015/08/27 Javascript
JS实现超精简响应鼠标显示二级菜单代码
2015/09/12 Javascript
AngularJS实现单一页面内设置跳转路由的方法
2017/06/28 Javascript
vue.js数据绑定的方法(单向、双向和一次性绑定)
2017/07/13 Javascript
vue-cli脚手架-bulid下的配置文件
2018/03/27 Javascript
vue多页面项目中路由使用history模式的方法
2019/09/23 Javascript
微信小程序中的video视频实现 自定义播放按钮、封面图、视频封面上文案
2020/01/02 Javascript
微信小程序录音实现功能并上传(使用node解析接收)
2020/02/26 Javascript
viewer.js一个强大的基于jQuery的图像查看插件(支持旋转、缩放)
2020/04/01 jQuery
[02:29]大剑、皮鞭、女装,这届DOTA2勇士令状里都有
2020/07/17 DOTA
Python语言描述最大连续子序列和
2017/12/05 Python
Python cookbook(数据结构与算法)从序列中移除重复项且保持元素间顺序不变的方法
2018/03/13 Python
python 日期排序的实例代码
2019/07/11 Python
Python如何实现定时器功能
2020/05/28 Python
解决Pytorch自定义层出现多Variable共享内存错误问题
2020/06/28 Python
使用CSS3制作饼状旋转载入效果的实例
2015/06/23 HTML / CSS
移动端Web页面的CSS3 flex布局快速上手指南
2016/05/31 HTML / CSS
香港化妆品经销商:我的公主
2016/08/05 全球购物
小学教师的自我评价范例
2013/10/31 职场文书
营销总监岗位职责范本
2014/02/26 职场文书
建议书怎么写
2014/03/12 职场文书
节水标语大全
2014/06/11 职场文书
寒山寺导游词
2015/02/03 职场文书
2015年党总支工作总结
2015/05/25 职场文书
2015年语言文字工作总结
2015/07/23 职场文书
Flask使用SQLAlchemy实现持久化数据
2021/07/16 Python
小程序实现悬浮按钮的全过程记录
2021/10/16 HTML / CSS