在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 相关文章推荐
jsp js鼠标移动到指定区域显示选项卡离开时隐藏示例
Jun 14 Javascript
js取消单选按钮选中并判断对象是否为空
Nov 14 Javascript
JS去除字符串两端空格的简单实例
Dec 27 Javascript
JavaScript动态修改网页元素内容的方法
Mar 21 Javascript
js实现分享到随页面滚动而滑动效果的方法
Apr 10 Javascript
深入解析桶排序算法及Node.js上JavaScript的代码实现
Jul 06 Javascript
js监听键盘事件的方法_原生和jquery的区别详解
Oct 10 Javascript
从零开始学习Node.js系列教程四:多页面实现数学运算的client端和server端示例
Apr 13 Javascript
bootstrap switch开关组件使用方法详解
Aug 22 Javascript
angular.js实现购物车功能
Oct 23 Javascript
Mint UI 基于 Vue.js 移动端组件库
Nov 07 Javascript
详解vuex状态管理模式
Nov 01 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 数组教程 定义数组
2009/10/23 PHP
写php分页时出现的Fatal error的解决方法
2011/04/18 PHP
PHP常用日期加减计算方法实例小结
2018/07/31 PHP
JavaScript 核心参考教程 内置对象
2009/10/13 Javascript
JS获取select-option-text_value的方法
2013/12/26 Javascript
将中国标准时间转换成标准格式的代码
2014/03/20 Javascript
全面解析Bootstrap中transition、affix的使用方法
2016/05/30 Javascript
AngularJS内建服务$location及其功能详解
2016/07/01 Javascript
全面了解函数声明与函数表达式、变量提升
2016/08/09 Javascript
js显示动态时间的方法详解
2016/08/20 Javascript
利用jQuery插件imgAreaSelect实现图片上传裁剪(放大缩小)
2016/12/02 Javascript
react中使用swiper的具体方法
2018/05/15 Javascript
Vue-cli3项目配置Vue.config.js实战记录
2018/07/29 Javascript
详解小程序不同页面之间通讯的解决方案
2018/11/23 Javascript
python爬虫正则表达式之处理换行符
2018/06/08 Python
Python退火算法在高次方程的应用
2018/07/26 Python
python实现点击按钮修改数据的方法
2019/07/17 Python
WxPython实现无边框界面
2019/11/18 Python
python识别验证码的思路及解决方案
2020/09/13 Python
CSS3 函数技巧 用css 实现js实现的事情(clac Counters Tooltip)
2017/08/15 HTML / CSS
详解移动端html5页面长按实现高亮全选文本内容的兼容解决方案
2016/12/03 HTML / CSS
美国婴儿用品店:Babies”R”Us
2017/10/12 全球购物
伊芙丽官方旗舰店:中国淑女一线品牌
2017/12/01 全球购物
美国玩具公司:U.S.Toy
2018/05/19 全球购物
Spartoo瑞典:鞋子、包包和衣服
2018/09/15 全球购物
你的创业计划书怎样才能打动风投
2014/02/06 职场文书
渡河少年教学反思
2014/02/12 职场文书
大学生就业意向书范文
2014/04/01 职场文书
网站美工岗位职责
2014/04/02 职场文书
网络技术专业求职信
2014/07/13 职场文书
质量保证书怎么写
2015/02/27 职场文书
正规欠条模板
2015/07/03 职场文书
2016元旦主持人开场白
2015/12/03 职场文书
2019年大学毕业生个人自我鉴定范文大全
2019/03/21 职场文书
如何写好开幕词?
2019/06/24 职场文书
前端JavaScript大管家 package.json
2021/11/02 Javascript