浅谈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 顺便学习下CSS选择器 奇偶匹配nth-child(even)
May 24 Javascript
处理及遍历XML文档DOM元素属性及方法整理
Aug 23 Javascript
jQuery 设置 CSS 属性示例介绍
Jan 16 Javascript
jquery实现的导航固定效果
Apr 28 Javascript
jquery插件autocomplete用法示例
Jul 01 Javascript
AngularJs 利用百度地图API 定位当前位置 获取地址信息
Jan 18 Javascript
使用canvas及js简单生成验证码方法
Apr 02 Javascript
bootstrap 设置checkbox部分选中效果
Apr 20 Javascript
js实现多张图片延迟加载效果
Jul 17 Javascript
讲解vue-router之什么是动态路由
May 28 Javascript
微信小程序实现swiper切换卡内嵌滚动条不显示的方法示例
Dec 20 Javascript
js中int和string数据类型互相转化实例
Jan 16 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
php5.3中连接sqlserver2000的两种方法(com与ODBC)
2012/12/29 PHP
php生成带logo二维码方法小结
2016/04/08 PHP
今天你说520了吗?不仅有php表白书还有java表白神器
2016/05/20 PHP
PHP使用Redis实现Session共享的实现示例
2019/05/12 PHP
PHP如何使用cURL实现Get和Post请求
2020/07/11 PHP
Javascript JSQL,SQL无处不在,
2010/05/05 Javascript
基于jQuery的判断iPad、iPhone、Android是横屏还是竖屏的代码
2014/05/11 Javascript
jQuery实现限制textarea文本框输入字符数量的方法
2015/05/28 Javascript
jQuery点击按钮弹出遮罩层且内容居中特效
2015/12/14 Javascript
bootstrap datetimepicker日期插件使用方法
2017/01/13 Javascript
微信小程序实战之顶部导航栏(选项卡)(1)
2020/06/19 Javascript
原生js实现简单的模态框示例
2017/09/08 Javascript
jQuery+SpringMVC中的复选框选择与传值实例
2018/01/08 jQuery
使用bootstrap实现下拉框搜索功能的实例讲解
2018/08/10 Javascript
解决Vue+Element ui开发中碰到的IE问题
2018/09/03 Javascript
createObjectURL方法实现本地图片预览
2019/09/30 Javascript
js实现左右轮播图
2020/01/09 Javascript
JS 数组基本用法入门示例解析
2020/01/16 Javascript
原生JavaScript实现的无缝滚动功能详解
2020/01/17 Javascript
javascript中contains是否包含功能实现代码(扩展字符、数组、dom)
2020/04/07 Javascript
vue+echarts实现动态折线图的方法与注意
2020/09/01 Javascript
Python 第一步 hello world
2009/09/25 Python
用Python解析XML的几种常见方法的介绍
2015/04/09 Python
Python中音频处理库pydub的使用教程
2017/06/07 Python
python实现RabbitMQ的消息队列的示例代码
2018/11/08 Python
Python Django切换MySQL数据库实例详解
2019/07/16 Python
python数据处理之如何选取csv文件中某几行的数据
2019/09/02 Python
完美解决Pycharm中matplotlib画图中文乱码问题
2021/01/11 Python
Canvas高级路径操作之拖拽对象的实现
2019/08/05 HTML / CSS
Dyson戴森波兰官网:Dyson.pl
2019/08/05 全球购物
机电专业大学生求职信
2013/10/04 职场文书
优秀学生获奖感言
2014/02/15 职场文书
离婚协议书格式
2014/11/21 职场文书
2015年置业顾问工作总结
2015/04/07 职场文书
学生会主席任命书
2015/09/21 职场文书
你需要掌握的20个Python常用技巧
2022/02/28 Python