在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 相关文章推荐
Javascript-Mozilla和IE中的一个函数直接量的问题
Jan 09 Javascript
在JavaScript中使用inline函数的问题
Mar 08 Javascript
js点击更换背景颜色或图片的实例代码
Jun 25 Javascript
JQuery CheckBox(复选框)操作方法汇总
Apr 15 Javascript
jQuery插件Zclip实现完美兼容个浏览器点击复制内容到剪贴板
Apr 30 Javascript
JavaScript给input的value赋值引发的关于基本类型值和引用类型值问题
Dec 07 Javascript
浅谈JS中的bind方法与函数柯里化
Aug 10 Javascript
浅谈js基础数据类型和引用类型,深浅拷贝问题,以及内存分配问题
Sep 02 Javascript
简单谈谈JS中的正则表达式
Sep 11 Javascript
LayUI动态设置checkbox不显示的解决方法
Sep 02 Javascript
vue iview的菜单组件Mune 点击不高亮的解决方案
Nov 01 Javascript
js实现鼠标点击页面弹出自定义文字效果
Dec 24 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中array_slice函数用法实例详解
2014/11/25 PHP
php注册审核重点解析(数据访问)
2017/05/23 PHP
JavaScript Event学习第五章 高级事件注册模型
2010/02/07 Javascript
数组Array进行原型prototype扩展后带来的for in遍历问题
2010/02/07 Javascript
JQuery对checkbox操作 (循环获取)
2011/05/20 Javascript
服务器端的JavaScript脚本 Node.js 使用入门
2012/03/07 Javascript
JavaScript:new 一个函数和直接调用函数的区别分析
2013/07/10 Javascript
jQuery获取选中内容及设置元素属性的方法
2014/07/09 Javascript
使用jquery菜单插件HoverTree仿京东无限级菜单
2014/12/18 Javascript
AngularJS控制器详解及示例代码
2016/08/16 Javascript
Bootstrap禁用响应式布局的实现方法
2017/03/09 Javascript
JS实现图片放大镜插件详解
2017/11/06 Javascript
vue项目中用cdn优化的方法
2018/01/03 Javascript
javascript中的隐式调用
2018/02/10 Javascript
javascript闭包的使用之按钮切换功能
2018/08/30 Javascript
详解在vue-cli项目下简单使用mockjs模拟数据
2018/10/19 Javascript
vue router总结 $router和$route及router与 router与route区别
2019/07/05 Javascript
jQuery中DOM常见操作实例小结
2019/08/01 jQuery
如何利用vue实现波谱拟合详解
2020/11/05 Javascript
python django 访问静态文件出现404或500错误
2017/01/20 Python
python 默认参数相关知识详解
2019/09/18 Python
Python(PyS60)实现简单语音整点报时
2019/11/18 Python
在django中自定义字段Field详解
2019/12/03 Python
python实现一个点绕另一个点旋转后的坐标
2019/12/04 Python
pytorch 自定义卷积核进行卷积操作方式
2019/12/30 Python
解决Keyerror ''acc'' KeyError: ''val_acc''问题
2020/06/18 Python
python中id函数运行方式
2020/07/03 Python
Python进行统计建模
2020/08/10 Python
利用Python如何画一颗心、小人发射爱心
2021/02/21 Python
Baby Tulai澳大利亚:美国婴儿背带品牌
2018/10/15 全球购物
伦敦鲜花递送:Flower Station
2021/02/03 全球购物
Java面试中常遇到的问题,也是需要注意的几点
2013/08/30 面试题
小学毕业感言150字
2014/02/05 职场文书
2015年导购员工作总结
2015/04/25 职场文书
家长会开场白和结束语
2015/05/29 职场文书
Java图书管理系统,课程设计必用(源码+文档)
2021/06/30 Java/Android