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
返回處理後的結果。
在未了解測試以前,我一直覺得測試是枯燥無聊的。瞭解過快照測試後,我發現測試其實蠻有趣且實用,同時由衷地感嘆快照測試的巧妙之處。若是這個簡單的案例能讓你瞭解快照測試的做用及使用方法,就是我最大的收穫。
若是有問題或錯誤之處,歡迎指出交流。