Vue 應用單元測試的策略與實踐 04 - Vuex 單元測試

本文首發於 Vue 應用單元測試的策略與實踐 04 - Vuex 單元測試 | 呂立青的博客javascript

歡迎關注知乎專欄 —— 前端的逆襲(凡可 JavaScript,終將 JavaScript。)html

歡迎關注個人博客知乎GitHub掘金前端


本文的目標

2.2 在Vue應用的單元測試中,對 Vuex store 該如何測試?如何測試與 Vue 組件之間的交互?vue

// Given
一個有基本的UT知識和Vue組件單元測試經驗的開發者🚶
// When
當他🚶閱讀和練習本文的Vuex單元測試的部分
// Then
他可以對Vuex概念的理解更加深刻,且知道 `Redux-like` 架構的好處
他可以合理測試vuex store的mutation、getter中的業務邏輯和異步action
他可以測試組件如何正確讀取store中的state以及dispatch action
複製代碼

如何理解 Vuex 模式?

Vuex 的前車可鑑

Vuex 是一個專爲 Vue.js 應用程序開發的狀態管理模式。它採用集中式存儲管理應用的全部組件的狀態,並以相應的規則保證狀態以一種可預測的方式發生變化。java

古人說「讀史讓人明智」,學習歷史是爲了更好得前行,爲了可以認識如今,看清將來。讓咱們來看看 Vuex 的歷史,Vuex 借鑑於 Redux,而 Redux 的實現構想則最初出身於 Flux ,這是一個由 Facebook 爲其應用所設計的應用程序架構。Flux 模式在 JavaScript 應用裏像是找到了新家同樣,但其實只是借鑑了領域驅動設計 (DDD) 和命令-查詢職責分離 (CQRS)。git

CQRS 與 Flux 架構

描述 Flux 最廣泛的一種的方式就是將其與 Model-View-Controller (MVC) 架構進行對比。github

在 MVC 當中,一個 Model 能夠被多個 Views 讀取,而且能夠被多個 Controllers 進行更新。在大型應用當中,單個 Model 會致使多個 Views 去通知 Controllers,並可能觸發更多的 Model 更新,這樣結果就會變得很是複雜。vuex

mvc-diagram

而 Flux 以及咱們要學習的 Vuex 則是試圖經過強制單向數據流來解決這個複雜度。在這種架構當中,Views 查詢 Stores(而不是 Models),而且用戶交互將會觸發 Actions,Actions 則會被提交到一個集中的 Dispatcher 當中。當 Actions 被派發以後,Stores 將會隨之更新本身而且通知 Views 進行修改。這些 Store 當中的修改會進一步促使 Views 查詢新的數據。api

flux-diagram

MVC 和 Flux 最大的不一樣就是查詢和更新的分離。在 MVC 中,Model 同時能夠被 Controller 更新而且被 View 所查詢。在 Flux 裏,View 從 Store 獲取的數據是隻讀的。而 Stores 只能經過 Actions 被更新,這就會影響 Store 自己而不是那些只讀的數據。架構

以上所描述的模式很是接近於由 Greg Young 第一次所提出的 CQRS:

  1. 若是一個方法修改了這個對象的狀態,那就是一個 command(命令),而且必定不能返回值。
  2. 若是一個方法返回了一些值,那就是一個 query(查詢),而且必定不能修改狀態。

Vuex 背後的基本思想

因此說, Vuex 就是把組件的共享狀態 「state」 抽取出來,以一個全局 「store」 的單例模式統一管理。在這種模式下,咱們的組件樹構成了一個巨大的「視圖」,無論在樹的哪一個位置,任何組件都能獲取狀態或者觸發行爲。

另外,隔離狀態管理可以得到不少好處,固然也須要強制遵照必定的規則:

  1. Vuex 的狀態存儲是響應式的。當 Vue 組件從 store 中讀取狀態的時候,若 store 中的狀態發生變化,那麼相應的組件也會相應地獲得高效更新。這也就是 CQRS 中 query(查詢)的一種實現。
  2. 你不能直接改變 store 中的狀態。改變 store 中的狀態的惟一途徑就是顯式地提交 (commit) mutation,這樣使得咱們能夠方便地跟蹤每個狀態的變化。這也就是 CQRS 中 command(命令)的一種實現。

如何對 Vuex 進行單元測試

得益於 Vuex 可以將 Vue 應用的共享狀態進行隔離,咱們的代碼也所以變得更加結構化且易於維護,Vuex 中的 mutation、action 和 getter 都被放在了合理的位置,承擔不一樣的職責 ,這也使得對它們進行單元測試變得容易不少。

mutations 測試

Mutation 很容易被測試,由於它們僅僅是一些徹底依賴參數的函數。最爲簡單的 mutation 測試,僅一一對應保存數據切片。此種 mutation 能夠不須要測試覆蓋,由於基本由架構簡單和邏輯簡單保證,不須要靠讀測試用例來理解。而一個較爲複雜、具有測試價值的 mutation 在保存數據的同時,還可能進行了合併、去重等操做。

// count.js
const state = { ... }
const actions = { ... }
export const mutations = {
  increment: state => state.count++
}
// count.test.js
import { mutations } from './store'

// 解構 `mutations`
const { increment } = mutations

describe('mutations', () => {
  it('INCREMENT', () => {
    // 模擬狀態
    const state = { count: 0 }
    // 應用 mutation
    increment(state)
    // 斷言結果
    expect(state.count).toEqual(1)
  })
})
複製代碼

actions 測試

Action 應對起來略微棘手,由於它們可能須要調用外部的 API。當測試 action 的時候,咱們須要增長一個 mocking 服務層——例如,咱們能夠把 API 調用抽象成服務,而後在測試文件中用 mock 服務響應所指望的 API 調用。

// product.js
import shop from '../api/shop'

export const actions = {
  getAllProducts({ commit }) {
    commit('REQUEST_PRODUCTS')
    shop.getProducts(products => {
      commit('RECEIVE_PRODUCTS', products)
    })
  }
}
複製代碼
// product.test.js
jest.mock('../api/shop', () => ({
  getProducts: jest.fn(() => /* mocked response */),
}))

describe('actions', () => {
  it('getAllProducts', () => {
    const commit = jest.spy()
    const state = {}
    
    actions.getAllProducts({ commit, state })
    
    expect(commit.args).toEqual([
      ['REQUEST_PRODUCTS'],
      ['RECEIVE_PRODUCTS', { /* mocked response */ }]
    ])
  })
})
複製代碼

getters 測試

getter 的測試與 mutation 同樣直截了當。getters 也是比較重邏輯的地方,而且它也是一個純函數,與 mutations 測試享受一樣待遇:純淨的輸入輸出,簡易的測試準備。下面來看一個稍微簡單點的 getters 測試用例:

// product.js
export const getters = {
  filteredProducts (state, { filterCategory }) {
    return state.products.filter(product => {
      return product.category === filterCategory
    })
  }
}
複製代碼
// product.test.js
import { expect } from 'chai'
import { getters } from './getters'

describe('getters', () => {
  it('filteredProducts', () => {
    // 模擬狀態
    const state = {
      products: [
        { id: 1, title: 'Apple', category: 'fruit' },
        { id: 2, title: 'Orange', category: 'fruit' },
        { id: 3, title: 'Carrot', category: 'vegetable' }
      ]
    }
    // 模擬 getter
    const filterCategory = 'fruit'

    // 獲取 getter 的結果
    const result = getters.filteredProducts(state, { filterCategory })

    // 斷言結果
    expect(result).to.deep.equal([
      { id: 1, title: 'Apple', category: 'fruit' },
      { id: 2, title: 'Orange', category: 'fruit' }
    ])
  })
})
複製代碼

Vue 組件和 Vuex store 的交互

前面咱們講完了 Vuex 單元測試所須要的基本知識,而 Vue 組件須要從 Vuex store 讀取狀態或者是發送 action 改變 store 狀態的時候,又該如何測試他們之間的交互呢?接下來就來聊聊如何用 Vue Test Utils 測試 Vue 組件中的 Vuex。

站在單元測試的角度,其實咱們在測試 Vue 組件(單元)的時候不須要關心 Vuex store 長什麼樣子,咱們只須要知道 Vuex store 當中的這些 action 將會在適當的時機觸發,以及它們觸發時的預期行爲是什麼。

<template>
  <div class="app">
    <div class="price">amount: ${{$store.state.price}}</div>
    <button @click="actionClick()">Buy</button>
  </div>
</template>

<script> import { mapActions } from 'vuex' export default { methods: { ...mapActions([ 'actionClick' ]), } } </script>
複製代碼

在單元測試的時候,shallowMount(淺渲染)方法接受一個掛載 options,能夠用來給 Vue 組件傳遞一個僞造的 store。而後咱們就可使用 Jest 模擬一個 action 的行爲再傳給 store,而 actionClick 這個僞造函數可以讓咱們去斷言該 action 是否被調用過。因此咱們在測試 action 的時候就能夠只關心 action 的觸發,而至於觸發以後對 store 作了什麼事情咱們就不須要再關心了,由於 Vuex 的單元測試會涵蓋相關的代碼邏輯。

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'

const fakeStore = new Vuex.Store({
  state: {},
  actions: {
    actionClick: jest.fn()
  }
})

const localVue = createLocalVue()
localVue.use(Vuex)

it('當按鈕被點擊時候調用「actionClick」的 action', () => {
    const wrapper = shallowMount(Actions, { store: fakeStore, localVue })
    wrapper.find('button').trigger('click')
    expect(actions.actionClick).toHaveBeenCalled()
})
複製代碼

須要注意的是,在這裏咱們是把 Vuex store 傳遞給一個 localVue,而不是傳遞給基礎的 Vue 構造函數。這是由於咱們不想影響到全局的 Vue 構造函數,若是直接使用 Vue.use(Vuex) 會讓Vue 的原型上會增長 $store 屬性從而影響到其餘的單元測試。而 localVue 則是一個獨立做用域的 Vue 構造函數,咱們能夠對其進行任意的改動。

固然咯,除了 mock 掉 actions,Vuex store 裏面的任何內容咱們均可以將其模擬出來,好比 state 或者 getters:

import { shallowMount, createLocalVue } from '@vue/test-utils'
import Vuex from 'vuex'

const fakeStore = new Vuex.Store({
  state: {
    price: '998'
  },
  getters: {
    clicks: () => 2,
    inputValue: () => 'input'
  }
})

const localVue = createLocalVue()
localVue.use(Vuex)

it('在app中渲染價格和「state.inputValue」', () => {
  const wrapper = shallowMount(Components, { store: fakeStore, localVue })
  expect(wrapper.find('p').text()).toBe('input')  
  expect(wrapper.find('.price').text()).stringContaining('$998')
})
複製代碼

總結一下

總之呢,不要測試 Vue 組件和 Vuex store 交互的時候引入一個真實的 Store,那樣就再也不是單元測試了,還記得咱們在第二篇單元測試基礎中所提到的社交型(Social Tests)仍是獨立型(Solitary Tests)測試單元嗎?Vuex 等 Redux-like 架構在前端應用中的 「狀態管理模式」 ,已經將 View 視圖層和 State 數據層儘量合理得拆分與隔離,那麼單元測試就只須要分別測試 Vue 和 Vuex,從而就能保證 Vue 組件和數據流按照預期那樣工做。

未完待續……

## 單元測試基礎

  • [x] ### 單元測試與自動化的意義
  • [x] ### 爲何選擇 Jest
  • [x] ### Jest 的基本用法
  • [x] ### 該如何測試異步代碼?

## Vue 單元測試

  • [x] ### Vue 組件的渲染方式
  • [x] ### Wrapper find() 方法與選擇器
  • [x] ### UI 組件交互行爲的測試

## Vuex 單元測試

  • [x] ### CQRS 與 Redux-like 架構
  • [x] ### 如何對 Vuex 進行單元測試
  • [x] ### Vue組件和Vuex store的交互

## Vue應用測試策略

  • [ ] ### 單元測試的特色及其位置
  • [ ] ### 單元測試的關注點
  • [ ] ### 應用測試的測試策略
相關文章
相關標籤/搜索