浅谈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在对象中缓存选择器的简单方法
Jun 30 Javascript
Javascript函数的参数
Jul 16 Javascript
深入理解jquery中extend的实现
Dec 22 Javascript
Vue.js自定义指令的用法与实例解析
Jan 18 Javascript
js指定步长实现单方向匀速运动
Jul 17 Javascript
详解node nvm进行node多版本管理
Oct 21 Javascript
浅谈针对Vue相同路由不同参数的刷新问题
Sep 29 Javascript
vue组件从开发到发布的实现步骤
Nov 11 Javascript
vue 点击展开显示更多(点击收起部分隐藏)
Apr 09 Javascript
微信小程序框架的页面布局代码
Aug 17 Javascript
微信小程序实现多选框全选与反全选及购物车中删除选中的商品功能
Dec 17 Javascript
比较node.js和Deno
Apr 27 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模板类代码
2008/09/07 PHP
PHP中把对象转换为关联数组代码分享
2015/04/09 PHP
Yii2.0高级框架数据库增删改查的一些操作
2015/11/16 PHP
php实现的http请求封装示例
2016/11/08 PHP
PHP基于pdo的数据库操作类【可支持mysql、sqlserver及oracle】
2018/05/21 PHP
Yii框架的redis命令使用方法简单示例
2019/10/15 PHP
cnblogs TagCloud基于jquery的实现代码
2010/06/11 Javascript
JavaScript 放大镜 放大倍率和视窗尺寸
2011/05/09 Javascript
js获取url参数代码实例分享(JS操作URL)
2013/12/13 Javascript
基于jquery animate操作css样式属性小结
2015/11/27 Javascript
基于jQuery1.9版本如何判断浏览器版本类型
2016/01/12 Javascript
javascript HTML5 Canvas实现圆盘抽奖功能
2016/04/11 Javascript
Web 开发中Ajax的Session 超时处理方法
2017/01/19 Javascript
three.js实现围绕某物体旋转
2017/01/25 Javascript
Vuex简单入门
2017/04/19 Javascript
vue-router 路由基础的详解
2017/10/17 Javascript
jQuery实现左右两个列表框的内容相互移动功能示例
2019/01/27 jQuery
Node.js Stream ondata触发时机与顺序的探索
2019/03/08 Javascript
[01:48]完美圣典齐天大圣至宝宣传片
2016/12/17 DOTA
Python 正则表达式入门(中级篇)
2016/12/07 Python
详解python上传文件和字符到PHP服务器
2017/11/24 Python
Python数据结构与算法之图的基本实现及迭代器实例详解
2017/12/12 Python
python实现决策树
2017/12/21 Python
python批量查询、汉字去重处理CSV文件
2018/05/31 Python
python寻找list中最大值、最小值并返回其所在位置的方法
2018/06/27 Python
Python的argparse库使用详解
2018/10/09 Python
解决Tensorflow 内存泄露问题
2020/02/05 Python
安装多个版本的TensorFlow的方法步骤
2020/04/21 Python
python中plt.imshow与cv2.imshow显示颜色问题
2020/07/16 Python
python中使用.py配置文件的方法详解
2020/11/23 Python
Python 爬虫批量爬取网页图片保存到本地的实现代码
2020/12/24 Python
历史系毕业生自荐信
2013/10/28 职场文书
《钱学森》听课反思
2014/03/01 职场文书
纪律教育学习月活动总结
2014/08/27 职场文书
2014年个人思想工作总结
2014/11/27 职场文书
学生早退检讨书(范文)
2019/08/19 职场文书