snapshot测试又称快照测试,能够直观地反映出组件UI是否发生了未预见到的变化。snapshot如字面上所示,直观描述出组件的样子。经过对比先后的快照,能够很快找出UI的变化之处。css
第一次运行快照测试时会生成一个快照文件。以后每次执行测试的时候,会生成一个快照,而后对比最初生成的快照文件,若是没有发生改变,则经过测试。不然测试不经过,同时会输出结果,对比不匹配的地方。html
jest中的快照文件觉得snap
拓展名结尾,格式以下(ps: 在没有了解以前,我还觉得是快照文件是截图)。一个快照文件中能够包含多个快照,快照的格式实际上是HTML字符串,对于UI组件,其HTML会反映出其内部的state。每次测试只须要对比字符串是否符合初始快照便可。vue
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
更新快照。node
› 1 snapshot failed from 1 test suite. Inspect your code changes or re-run jest with
-u
to update them.
生成快照时须要渲染并挂载组件,在Vue中可使用官方的单元测试实用工具Vue Test Utils。git
Vue Test Utils 提供了mount
、shallowMount
这两个方法,用于建立一个包含被挂载和渲染的 Vue 组件的 Wrapper。component
是一个vue组件,options
是实例化Vue时的配置,包括挂载选项和其余选项(非挂载选项,会将它们经过extend
覆写到其组件选项),结果返回一个包括了一个挂载组件或 vnode,以及测试该组件或 vnode 的方法的Wrapper实例。github
mount(component:{Component}, options:{Object})
shallowMount
与mount
不一样的是被存根的子组件,详细请戳文档。npm
Wrapper上的丰富的属性和方法,足以应付本文中的测试需求。html()
方法返回Wrapper DOM 节点的 HTML 字符串。find()
和findAll()
能够查找Wrapper里的DOM节点或Vue组件,可用于查找监听事件的元素。trigger
能够在DOM节点/组件上触发一个事件。json
结合上述的方法,咱们能够完成一个模拟事件触发的快照测试。api
细心的读者可能会发现,咱们平时在使用Vue时,数据更新后视图并不会当即更新,须要在nextTick回调中处理更新完成后的任务。但在 Vue Test Utils 中,为简化用法,更新是同步的,因此无需在测试中使用 Vue.nextTick 来等待 DOM 更新。数组
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.config.js
文件里,能够保存在package.json
里,用键名jest
表示,同时也容许行内配置。
介绍几个经常使用的配置。
查找Jest配置的目录,默认是pwd。
jest查找测试文件的匹配规则,默认是[ "**/__tests__/**/*.js?(x)", "**/?(*.)+(spec|test).js?(x)" ]
。默认查找在__test__
文件夹中的js/jsx
文件和以.test/.spec
结尾的js/jsx
文件,同时包括test.js
和spec.js
。
生成的快照文件中HTML文本没有换行,是否能进行换行美化呢?答案是确定的。
能够在配置中添加snapshotSerializers
,接受一个数组,能够对匹配的快照文件作处理。jest-serializer-vue这个库作的就是这样任务。
若是你想要实现这个本身的序列化任务,须要实现的方法有test
和print
。test
用于筛选处理的快照,print
返回处理后的结果。
在未了解测试以前,我一直觉得测试是枯燥无聊的。了解过快照测试后,我发现测试其实蛮有趣且实用,同时由衷地感叹快照测试的巧妙之处。若是这个简单的案例能让你了解快照测试的做用及使用方法,就是我最大的收获。
若是有问题或错误之处,欢迎指出交流。