在Vue项目中使用snapshot测试的具体使用


Posted in Javascript onApril 16, 2019

snapshot介绍

snapshot测试又称快照测试,可以直观地反映出组件UI是否发生了未预见到的变化。snapshot如字面上所示,直观描述出组件的样子。通过对比前后的快照,可以很快找出UI的变化之处。

第一次运行快照测试时会生成一个快照文件。之后每次执行测试的时候,会生成一个快照,然后对比最初生成的快照文件,如果没有发生改变,则通过测试。否则测试不通过,同时会输出结果,对比不匹配的地方。

jest中的快照文件以为snap拓展名结尾,格式如下(ps: 在没有了解之前,我还以为是快照文件是截图)。一个快照文件中可以包含多个快照,快照的格式其实是HTML字符串,对于UI组件,其HTML会反映出其内部的state。每次测试只需要对比字符串是否符合初始快照即可。

exports[`button 1`] = `"<div><span class=\\"count\\">1</span> <button>Increment</button> <button class=\\"desc\\">Descrement</button> <button class=\\"custom\\">not emitted</button></div>"`;

snapshot测试不通过的原因有两个。一个原因是组件发生了未曾预见的变化,此时应检查代码。另一个原因是组件更新而快照文件并没有更新,此时要运行jest -u更新快照。

› 1 snapshot failed from 1 test suite. Inspect your code changes or re-run jest with -u to update them.

结合Vue进行snapshot测试

生成快照时需要渲染并挂载组件,在Vue中可以使用官方的单元测试实用工具Vue Test Utils。

Vue Test Utils 提供了mount、shallowMount这两个方法,用于创建一个包含被挂载和渲染的 Vue 组件的 Wrapper。component是一个vue组件,options是实例化Vue时的配置,包括挂载选项和其他选项(非挂载选项,会将它们通过extend覆写到其组件选项),结果返回一个包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法的Wrapper实例。

mount(component:{Component}, options:{Object})

shallowMount与mount不同的是被存根的子组件,详细请戳文档。

Wrapper上的丰富的属性和方法,足以应付本文中的测试需求。html()方法返回Wrapper DOM 节点的 HTML 字符串。find()和findAll()可以查找Wrapper里的DOM节点或Vue组件,可用于查找监听事件的元素。trigger可以在DOM节点/组件上触发一个事件。

结合上述的方法,我们可以完成一个模拟事件触发的快照测试。

细心的读者可能会发现,我们平时在使用Vue时,数据更新后视图并不会立即更新,需要在nextTick回调中处理更新完成后的任务。但在 Vue Test Utils 中,为简化用法,更新是同步的,所以无需在测试中使用 Vue.nextTick 来等待 DOM 更新。

demo演示

Vue Test Utils官方文档中提供了一个集成VTU和Jest的demo,不过这个demo比较旧,官方推荐用CLI3创建项目。

执行vue create vue-snapshot-demo创建demo项目,创建时要选择单元测试,提供的库有Mocha + Chai及Jest,在这里选择Jest.安装完成之后运行npm run serve即可运行项目。

本文中将用一个简单的Todo应用项目来演示。这个Todo应用有简单的添加、删除和修改Todo项状态的功能;Todo项的状态有已完成和未完成,已完成时不可删除,未完成时可删除;已完成的Todo项会用一条线横贯文本,未完成项会在鼠标悬浮时展示删除按钮。

组件简单地划分为Todo和TodoItem。TodoItem在Todo项未完成且触发mouseover事件时会展示删除按钮,触发mouseleave时则隐藏按钮(这样可以在快照测试中模拟事件)。TodoItem中有一个checkbox,用于切换Todo项的状态。Todo项完成时会有一个todo-finished类,用于实现删除线效果。

为方便这里只介绍TodoItem组件的代码和测试。

<template>
 <li
  :class="['todo-item', item.finished?'todo-finished':'']"
  @mouseover="handleItemMouseIn"
  @mouseleave="handleItemMouseLeave"
 >
  <input type="checkbox" v-model="item.finished">
  <span class="content">{{item.content}}</span>
  <button class="del-btn" v-show="!item.finished&&hover" @click="emitDelete">delete</button>
 </li>
</template>

<script>
export default {
 name: "TodoItem",
 props: {
  item: Object
 },
 data() {
  return {
   hover: false
  };
 },
 methods: {
  handleItemMouseIn() {
   this.hover = true;
  },
  handleItemMouseLeave() {
   this.hover = false;
  },
  emitDelete() {
   this.$emit("delete");
  }
 }
};
</script>
<style lang="scss">
.todo-item {
 list-style: none;
 padding: 4px 16px;
 height: 22px;
 line-height: 22px;
 .content {
  margin-left: 16px;
 }
 .del-btn {
  margin-left: 16px;
 }
 &.todo-finished {
  text-decoration: line-through;
 }
}
</style>

进行快照测试时,除了测试数据渲染是否正确外还可以模拟事件。这里只贴快照测试用例的代码,完整的代码戳我。

describe('TodoItem snapshot test', () => {
  it('first render', () => {
    const wrapper = shallowMount(TodoItem, {
      propsData: {
        item: {
          finished: true,
          content: 'test TodoItem'
        }
      }
    })
    expect(wrapper.html()).toMatchSnapshot()
  })

  it('toggle checked', () => {
    const renderer = createRenderer();
    const wrapper = shallowMount(TodoItem, {
      propsData: {
        item: {
          finished: true,
          content: 'test TodoItem'
        }
      }
    })
    const checkbox = wrapper.find('input');
    checkbox.trigger('click');
    renderer.renderToString(wrapper.vm, (err, str) => {
      expect(str).toMatchSnapshot()
    })
  })
  
  it('mouseover', () => {
    const renderer = createRenderer();
    const wrapper = shallowMount(TodoItem, {
      propsData: {
        item: {
          finished: false,
          content: 'test TodoItem'
        }
      }
    })
    wrapper.trigger('mouseover');
    renderer.renderToString(wrapper.vm, (err, str) => {
      expect(str).toMatchSnapshot()
    })
  })
})

这里有三个测试。第二个测试模拟checkbox点击,将Todo项从已完成切换到未完成,期待类todo-finished会被移除。第三个测试在未完成Todo项上模拟鼠标悬浮,触发mouseover事件,期待删除按钮会展示。

这里使用toMatchSnapshot()来进行匹配快照。这里生成快照文件所需的HTML字符串有wrapper.html()和Renderer.renderToString这两种方式,区别在于前者是同步获取,后者是异步获取。

测试模拟事件时,最好以异步方式获取HTML字符串。同步方式获取的字符串并不一定是UI更新后的视图。

尽管VTU文档中说所有的更新都是同步,但实际上在第二个快照测试中,如果使用expect(wrapper.html()).toMatchSnapshot(),生成的快照文件中Todo项仍有类todo-finished,期待的结果应该是没有类todo-finished,结果并非更新后的视图。而在第三个快照测试中,使用expect(wrapper.html()).toMatchSnapshot()生成的快照,按钮如期望展示,是UI更新后的视图。所以才不建议在DOM更新的情况下使用wrapper.html()获取HTML字符串。

下面是两种对比的结果,1是使用wrapper.html()生成的快照,2是使用Renderer.renderToString生成的。

exports[`TodoItem snapshot test mouseover 1`] = `<li class="todo-item"><input type="checkbox"> <span class="content">test TodoItem</span> <button class="del-btn" style="">delete</button></li>`;

exports[`TodoItem snapshot test mouseover 2`] = `<li class="todo-item"><input type="checkbox"> <span class="content">test TodoItem</span> <button class="del-btn">delete</button></li>`;

exports[`TodoItem snapshot test toggle checked 1`] = `<li class="todo-item todo-finished"><input type="checkbox"> <span class="content">test TodoItem</span> <button class="del-btn" style="display: none;">delete</button></li>`;

exports[`TodoItem snapshot test toggle checked 2`] = `<li class="todo-item"><input type="checkbox"> <span class="content">test TodoItem</span> <button class="del-btn" style="display:none;">delete</button></li>`;

这里使用vue-server-renderer提供的createRenderer来生成一个Renderer实例,实例方法renderToString来获取HTML字符串。这种是典型的回调风格,断言语句在回调中执行即可。

// ...
  wrapper.trigger('mouseover');
  renderer.renderToString(wrapper.vm, (err, str) => {
    expect(str).toMatchSnapshot()
  })

如果不想使用这个库,也可以使用VTU中提供的异步案例。由于wrapper.html()是同步获取,所以获取操作及断言语句需要在Vue.nextTick()返回的Promise中执行。

// ...
  wrapper.trigger('mouseover');
  Vue.nextTick().then(()=>{
    expect(wrapper.html()).toMatchSnapshot()
  })

观察测试结果

执行npm run test:unit或yarn test:unit运行测试。

初次执行,终端输出会有Snapshots: 3 written, 3 total这一行,表示新增三个快照测试,并生成初始快照文件。

› 3 snapshots written.
Snapshot Summary
 › 3 snapshots written from 1 test suite.

Test Suites: 1 passed, 1 total
Tests:    7 passed, 7 total
Snapshots:  3 written, 3 total
Time:    2.012s
Ran all test suites.
Done in 3.13s.

快照文件如下示:

// Jest Snapshot v1, https://goo.gl/fbAQLP

exports[`TodoItem snapshot test first render 1`] = `<li class="todo-item todo-finished"><input type="checkbox"> <span class="content">test TodoItem</span> <button class="del-btn" style="display: none;">delete</button></li>`;

exports[`TodoItem snapshot test mouseover 1`] = `<li class="todo-item"><input type="checkbox"> <span class="content">test TodoItem</span> <button class="del-btn">delete</button></li>`;

exports[`TodoItem snapshot test toggle checked 1`] = `<li class="todo-item"><input type="checkbox"> <span class="content">test TodoItem</span> <button class="del-btn" style="display:none;">delete</button></li>`;

第二次执行测试后,输出中有Snapshots: 3 passed, 3 total,表示有三个快照测试成功通过,总共有三个快照测试。

Test Suites: 1 passed, 1 total
Tests:    7 passed, 7 total
Snapshots:  3 passed, 3 total
Time:    2s
Ran all test suites.
Done in 3.11s.

修改第一个快照中传入的content,重新运行测试时,终端会输出不匹配的地方,输出数据的格式与Git类似,会标明哪一行是新增的,哪一行是被删除的,并提示不匹配代码所在行。

- Snapshot
  + Received

  - <li class="todo-item todo-finished"><input type="checkbox"> <span class="content">test TodoItem</span> <button class="del-btn" style="display: none;">delete</button></li>
  + <li class="todo-item todo-finished"><input type="checkbox"> <span class="content">test TodoItem content change</span> <button class="del-btn" style="display: none;">delete</button></li>

   88 |       }
   89 |     })
  > 90 |     expect(wrapper.html()).toMatchSnapshot()
     |                ^
   91 |   })
   92 |
   93 |   it('toggle checked', () => {

   at Object.toMatchSnapshot (tests/unit/TodoItem.spec.js:90:32)

同时会提醒你检查代码是否错误或重新运行测试并提供参数-u以更新快照文件。

Snapshot Summary
 › 1 snapshot failed from 1 test suite. Inspect your code changes or re-run jest with `-u` to update them.

执行npm run test:unit -- -u或yarn test:unit -u更新快照,输出如下示,可以发现有一个快照测试的输出更新了。下次快照测试对照的文件是这个更新后的文件。

Test Suites: 1 passed, 1 total
Tests:    7 passed, 7 total
Snapshots:  1 updated, 2 passed, 3 total
Time:    2.104s, estimated 3s
Ran all test suites.
Done in 2.93s.

其他

除了使用toMatchSnapshot()外,还可以使用toMatchInlineSnapshot()。二者不同之处在于toMatchSnapshot()从快照文件中查找快照,而toMatchInlineSnapshot()则将传入的参数当成快照文件进行匹配。

配置Jest

Jest配置可以保存在jest.config.js文件里,可以保存在package.json里,用键名jest表示,同时也允许行内配置。

介绍几个常用的配置。

rootDir

查找Jest配置的目录,默认是pwd。

testMatch

jest查找测试文件的匹配规则,默认是[ "**/__tests__/**/*.js?(x)", "**/?(*.)+(spec|test).js?(x)" ]。默认查找在__test__文件夹中的js/jsx文件和以.test/.spec结尾的js/jsx文件,同时包括test.js和spec.js。

snapshotSerializers

生成的快照文件中HTML文本没有换行,是否能进行换行美化呢?答案是肯定的。

可以在配置中添加snapshotSerializers,接受一个数组,可以对匹配的快照文件做处理。jest-serializer-vue这个库做的就是这样任务。

如果你想要实现这个自己的序列化任务,需要实现的方法有test和print。test用于筛选处理的快照,print返回处理后的结果。

后记

在未了解测试之前,我一直以为测试是枯燥无聊的。了解过快照测试后,我发现测试其实蛮有趣且实用,同时由衷地感叹快照测试的巧妙之处。如果这个简单的案例能让你了解快照测试的作用及使用方法,就是我最大的收获。

如果有问题或错误之处,欢迎指出交流。

参考链接

vue-test-utils-jest-example
Jest - Snapshot Testing
Vue Test Utils
Vue SSR 指南

以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持三水点靠木。

Javascript 相关文章推荐
js中indexof的用法详细解析
Dec 24 Javascript
JavaScript eval() 函数介绍及应用示例
Jul 29 Javascript
js的touch事件的实际引用
Oct 13 Javascript
jQuery中empty()方法用法实例
Jan 16 Javascript
很不错的两款Bootstrap Icon图标选择组件
Jan 28 Javascript
Bootstrap中的表单验证插件bootstrapValidator使用方法整理(推荐)
Jun 21 Javascript
详解Vue 实例中的生命周期钩子
Mar 21 Javascript
vue v-model表单控件绑定详解
May 17 Javascript
vue系列之动态路由详解【原创】
Sep 10 Javascript
JS实现延迟隐藏功能的方法(类似QQ头像鼠标放上展示信息)
Dec 28 Javascript
JS重学系列之聊聊new操作符
Mar 04 Javascript
vue cli3 配置proxy代理无效的解决
Oct 30 Javascript
vue.js中使用echarts实现数据动态刷新功能
Apr 16 #Javascript
详解vue-cli 脚手架 安装
Apr 16 #Javascript
详解jquery和vue对比
Apr 16 #jQuery
JS使用百度地图API自动获取地址和经纬度操作示例
Apr 16 #Javascript
JQuery Ajax跨域调用和非跨域调用问题实例分析
Apr 16 #jQuery
vue+element UI实现树形表格带复选框的示例代码
Apr 16 #Javascript
JS实现根据详细地址获取经纬度功能示例
Apr 16 #Javascript
You might like
坏狼的PHP学习教程之第2天
2008/06/15 PHP
简单解决新浪SAE无法上传文件的问题
2015/05/13 PHP
PHP实现HTTP断点续传的方法
2015/06/17 PHP
javascript闭包的理解和实例
2010/08/12 Javascript
jQuery 选择器、DOM操作、事件、动画
2010/11/25 Javascript
JS打字效果的动态菜单代码分享
2015/08/21 Javascript
canvas实现手机端用来上传用户头像的代码
2016/10/20 Javascript
layui table 参数设置方法
2018/08/14 Javascript
JavaScript基于遍历操作实现对象深拷贝功能示例
2019/03/05 Javascript
tracking.js实现前端人脸识别功能
2020/04/16 Javascript
原生JS实现记忆翻牌游戏
2020/07/31 Javascript
vue 项目中当访问路由不存在的时候默认访问404页面操作
2020/08/31 Javascript
openlayers实现地图弹窗
2020/09/25 Javascript
vue 动态创建组件的两种方法
2020/12/31 Vue.js
[01:05:29]DOTA2-DPC中国联赛 正赛 PSG.LGD vs Aster BO3 第二场 1月24日
2021/03/11 DOTA
python smtplib模块实现发送邮件带附件sendmail
2018/05/22 Python
如何更优雅地写python代码
2019/07/02 Python
python sorted方法和列表使用解析
2019/11/18 Python
python对 MySQL 数据库进行增删改查的脚本
2020/10/22 Python
Pycharm安装Qt Design快捷工具的详细教程
2020/11/18 Python
Python存储读取HDF5文件代码解析
2020/11/25 Python
美国知名男士服饰品牌:Brooks Brothers(布克兄弟)
2016/08/25 全球购物
蔻驰法国官网:COACH法国
2018/11/14 全球购物
波兰在线杂货店:Polski Koszyk
2019/11/02 全球购物
计算 s=(x*y)1/2,用两个宏定义来实现
2016/08/11 面试题
linux系统都有哪些运行级别
2012/04/15 面试题
外贸英语毕业生自荐信
2013/11/14 职场文书
数控机械专业个人的自我评价
2014/01/02 职场文书
《九寨沟》教学反思
2014/04/08 职场文书
一般纳税人申请报告
2015/05/18 职场文书
检察院起诉书
2015/05/20 职场文书
毕业论文指导老师意见
2015/06/04 职场文书
学校扫黄打非工作总结
2015/10/15 职场文书
vue.js Router中嵌套路由的实用示例
2021/06/27 Vue.js
MySQL 聚合函数排序
2021/07/16 MySQL
人民币符号
2022/02/17 杂记