欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

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

程序员文章站 2022-11-05 22:36:05
snapshot介绍 snapshot测试又称快照测试,可以直观地反映出组件ui是否发生了未预见到的变化。snapshot如字面上所示,直观描述出组件的样子。通过对比...

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比较旧,官方推荐用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>`;

这里使用提供的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返回处理后的结果。

后记

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

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

参考链接


jest - snapshot testing
vue test utils
vue ssr 指南

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