Vue 測試速成班

  • 原文地址:dev.to/blacksonic/…
  • 原文做者:Gábor Soós
  • 譯者:馬雪琴
  • 聲明:本翻譯僅作學習交流使用,轉載請註明來源

在你快要完成一個項目時,忽然工程裏的不少地方都出現了 bug,你修完一個又冒出新的一個,就像在玩打地鼠遊戲同樣……幾輪下來,你會感到一團糟。此時有一個可讓你的項目再次發光的解救方案,那就是爲將要開發的和已經存在的特性編寫測試。編寫測試能夠保證功能特性沒有 bug。html

在本教程中,我將向你展現如何爲 Vue 應用程序編寫單元、集成和端到端測試。vue

有關更多測試示例,能夠查看個人 Vue TodoApp 實現ios

1. 類型

咱們能夠編寫三種類型的測試:單元測試、集成測試和端到端測試。下面這個金字塔能夠幫助咱們理解這些測試類型。git

在金字塔下端的測試寫起來更容易,運行起來更快,也更容易維護。可是,爲何咱們不能只寫單元測試呢?由於金字塔上端的測試能夠幫助咱們檢查系統裏的各個組件之間是否能很好地協同工做,使咱們對系統更有把握。github

單元測試只能被單獨使用在單個代碼單元(類、函數)裏;集成測試能夠檢查多個單元是否能按預期協同工做(組件層次結構、組件 + 存儲);端到端測試則是從外部世界觀察應用程序:瀏覽器及其交互。vue-router

2. 測試運行器

對於新的 Vue 項目,添加測試的最簡單方法是使用 Vue CLI。在生成項目(執行 vue create myapp)時,你必須手動選擇單元測試和 E2E 測試。vuex

安裝完成後,package.json 中將出現下面幾個附加依賴項:vue-cli

  • @vue/cli-plugin-unit-mocha: 使用 Mocha 進行單元/集成測試的插件
  • @vue/test-utils: 單元/集成測試的工具庫
  • chai: 斷言庫 Chai

從如今開始,單元/集成測試文件可使用 *.spec.js 後綴寫在 tests/unit 目錄中。測試的目錄不是硬連線的,你能夠用下面的命令行參數來修改它:json

vue-cli-service test:unit --recursive 'src/**/*.spec.js'
複製代碼

recursive 參數告訴測試運行器依據後面的通配符模式來搜索測試文件。axios

3. 單元測試

到目前爲止,一切順利,可是咱們尚未編寫任何測試。接下來咱們將編寫第一個單元測試!

describe('toUpperCase', () => {
  it('should convert string to upper case', () => {
    // 準備
    const toUpperCase = info => info.toUpperCase();

    // 操做
    const result = toUpperCase('Click to modify');

    // 斷言
    expect(result).to.eql('CLICK TO MODIFY');
  });
});
複製代碼

上面的例子驗證了 toUpperCase 函數是否將傳入的字符串轉換爲了大寫字母。

首先是準備工做,導入函數、實例化對象並設置其參數,讓目標對象(這裏是一個函數)進入一個可測試的狀態。而後操做該功能/方法。最後咱們對函數返回的結果進行斷言。

Mocha 提供了 describeit 兩個方法。describe 函數表示圍繞測試單元組織測試用例:測試單元能夠是類、函數、組件等。Mocha 沒有內置的斷言庫,因此咱們必須使用 Chai :它能夠設置對結果的指望。Chai 有許多不一樣的內置斷言,但沒有涵蓋全部用例,缺失的斷言能夠經過 Chai 的插件系統導入。

大多數時候,你還將爲組件層次結構以外的業務邏輯編寫單元測試,例如,狀態管理或後端 API 處理。

4. 組件展現

下一步是爲組件編寫集成測試。集成測試不僅是測試 Javascript 代碼,還會測試 DOM 和相應組件邏輯之間的交互。

// src/components/Footer.vue
<template>
  <p class="info">{{ info }}</p>
  <button @click="modify">Modify</button>
</template>
<script>
  export default {
    data: () => ({ info: 'Click to modify' }),
    methods: {
      modify() {
        this.info = 'Modified by click';
      }
    }
  };
</script>
複製代碼

咱們測試的第一個組件是一個渲染其狀態並在單擊按鈕時修改狀態的組件。

// test/unit/components/Footer.spec.js
import { expect } from 'chai';
import { shallowMount } from '@vue/test-utils';
import Footer from '@/components/Footer.vue';

describe('Footer', () => {
  it('should render component', () => {
    const wrapper = shallowMount(Footer);

    const text = wrapper.find('.info').text();
    const html = wrapper.find('.info').html();
    const classes = wrapper.find('.info').classes();
    const element = wrapper.find('.info').element;

    expect(text).to.eql('Click to modify');
    expect(html).to.eql('<p class="info">Click to modify</p>');
    expect(classes).to.eql(['info']);
    expect(element).to.be.an.instanceOf(HTMLParagraphElement);
  });
});
複製代碼

要在測試中渲染組件,咱們必須使用 Vue 測試工具庫中的 shallowMountmount。這兩個方法都會渲染組件,可是 shallowMount 不會渲染子組件(子元素將是空元素)。當須要引入某個組件進行測試時,咱們能夠以相對路徑引用 ../../../src/components/Footer.vue 或使用別名 @,路徑開頭的 @ 符號表示對源文件夾 src 的引用。

咱們可使用 find 選擇器在渲染的 DOM 中搜索並獲取它的 HTML、文本、類名或原生 DOM 元素。若是搜索的是一個可能不存在的片斷,咱們可使用 exists 方法判斷它是否存在。上述各類斷言只是爲了示意各類狀況,實際在測試用例中寫其中一個斷言就夠了。

5. 組件交互

咱們已經測試了 DOM 的渲染,但尚未與組件進行任何交互。咱們能夠經過 DOM 或組件實例與組件交互:

it('should modify the text after calling modify', () => {
  const wrapper = shallowMount(Footer);

  wrapper.vm.modify();

  expect(wrapper.vm.info).to.eql('Modified by click');
});
複製代碼

上面的例子展現瞭如何使用組件實例來實現交互。咱們可使用 vm 屬性訪問組件實例,還能夠經過組件實例訪問到組件 method 中的方法和 data 對象(狀態)裏的屬性。

另外一種方法是經過 DOM 與組件交互,咱們能夠觸發按鈕上的單擊事件並觀察是否顯示文本:

it('should modify the text after clicking the button', () => {
  const wrapper = shallowMount(Footer);

  wrapper.find('button').trigger('click');
  const text = wrapper.find('.info').text();

  expect(text).to.eql('Modified by click');
});
複製代碼

觸發 buttonclick 事件等同於在組件實例上調用 modify 方法。

6. 父子組件交互

上面咱們單獨測試了組件,但實際應用程序由多個部分組成。父組件經過 props 與子組件通訊,子組件經過觸發事件與父組件通訊。

咱們能夠經過修改傳入組件的 props 來更新組件的展現文案,並經過事件將改動通知給父組件。

export default {
  props: ['info'],
  methods: {
    modify() {
      this.$emit('modify', 'Modified by click');
    }
  }
};
複製代碼

在接下來的測試中,咱們須要把 props 做爲輸入,並監聽觸發的事件。

it('should handle interactions', () => {
  const wrapper = shallowMount(Footer, {
    propsData: { info: 'Click to modify' }
  });

  wrapper.vm.modify();

  expect(wrapper.vm.info).to.eql('Click to modify');
  expect(wrapper.emitted().modify).to.eql([
    ['Modified by click']
  ]);
});
複製代碼

shallowMountmount 方法的第二個參數是一個可選參數,咱們可使用 propsData 設置輸入的 props。觸發的事件能夠經過調用 emitted 方法得到,獲得的結果是一個對象,key 是事件的名稱,value 是事件參數數組。

6. store 集成

在前面的例子中,狀態都在組件內部。而在複雜的應用程序中,咱們須要在不一樣的位置訪問和改變相同的狀態。Vuex 是 Vue 的狀態管理庫,它能夠幫助你在一個地方組織狀態管理,並確保其可預測地發生變化。

const store = {
  state: {
    info: 'Click to modify'
  },
  actions: {
    onModify: ({ commit }, info) => commit('modify', { info })
  },
  mutations: {
    modify: (state, { info }) => state.info = info
  }
};
const vuexStore = new Vuex.Store(store);
複製代碼

上面的 store 有一個單一的狀態屬性,它與咱們在上面的組件中設置的同樣。咱們可使用 onModify 操做修改狀態,該操做將輸入參數傳遞給名爲 modify 的 mutation 來改變狀態。

首先,咱們能夠給 store 裏的每一個方法單獨編寫單元測試:

it('should modify state', () => {
  const state = {};

  store.mutations.modify(state, { info: 'Modified by click' });

  expect(state.info).to.eql('Modified by click');
});
複製代碼

咱們也能夠構建 store 來編寫集成測試,從而檢查總體是否能不拋出錯誤,正常運行:

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

it('should modify state', () => {
  const localVue = createLocalVue();
  localVue.use(Vuex);
  const vuexStore = new Vuex.Store(store);

  vuexStore.dispatch('onModify', 'Modified by click');

  expect(vuexStore.state.info).to.eql('Modified by click');
});
複製代碼

首先,咱們必須建立一個 Vue 的局部實例,而後使用 use 語句。若是咱們不調用 use 方法,將會拋出一個錯誤。經過建立 Vue 的局部副本,咱們還能夠避免污染全局對象。

咱們能夠經過 dispatch 方法改變 store。第一個參數表示調用哪一個 action;第二個參數做爲參數傳遞給 action。咱們能夠隨時經過 state 屬性檢查當前狀態。

當使用組件的 store 時,咱們必須將局部 Vue 實例和 store 實例傳遞給 mount 函數。

const wrapper = shallowMount(Footer, { localVue, store: vuexStore });
複製代碼

8. 路由

測試路由的設置與測試 store 有點相似,必須建立 Vue 實例的局部副本和路由實例,使用路由實例做爲插件,而後建立組件。

<div class="route">{{ $router.path }}</div>
複製代碼

上面這行組件模板將渲染當前路由路徑。在測試中,咱們能夠斷言這個元素的內容。

import VueRouter from 'vue-router';
import { createLocalVue } from '@vue/test-utils';

it('should display route', () => {
  const localVue = createLocalVue();
  localVue.use(VueRouter);
  const router = new VueRouter({
    routes: [
      { path: '*', component: Footer }
    ]
  });

  const wrapper = shallowMount(Footer, { localVue, router });
  router.push('/modify');
  const text = wrapper.find('.route').text();

  expect(text).to.eql('/modify');
});
複製代碼

咱們用 * 路徑爲組件添加了一個全匹配路由。有了 router 實例後,咱們還須要使用路由器的 push 方法爲應用程序設置導航。

建立全部路由可能會是一項耗時的任務,咱們能夠實現一個僞路由器,將其做爲一個 mock 數據傳遞:

it('should display route', () => {
  const wrapper = shallowMount(Footer, {
    mocks: {
      $router: {
        path: '/modify'
      }
    }
  });
  const text = wrapper.find('.route').text();

  expect(text).to.eql('/modify');
});
複製代碼

咱們也能夠在 mocks 中定義一個 $store 屬性來 mock store。

9. HTTP 請求

初始狀態一般是經過 HTTP 請求獲得的。咱們很容易在測試中完成真實的請求,但這會使得測試變得脆弱,而且對外部造成依賴。爲了不這種狀況,咱們能夠在運行時更改請求的實現。在運行時更改實現稱爲 mocking,咱們將使用 Sinon 這一 mocking 框架來實現。

const store = {
  actions: {
    async onModify({ commit }, info) {
      const response = await axios.post('https://example.com/api', { info });
      commit('modify', { info: response.body });
    }
  }
};
複製代碼

咱們在上面這段代碼中修改了 store 的實現:首先輸入參數經過 POST 請求被髮送,而後將該請求獲得的結果傳遞給 mutation。代碼變成了異步,並有了一個外部依賴項,外部依賴項將是咱們在運行測試以前必須更改(mock)的項。

import chai from 'chai';
import sinon from 'sinon';
import sinonChai from 'sinon-chai';
chai.use(sinonChai);

it('should set info coming from endpoint', async () => {
  const commit = sinon.stub();
  sinon.stub(axios, 'post').resolves({
    body: 'Modified by post'
  });

  await store.actions.onModify({ commit }, 'Modified by click');

  expect(commit).to.have.been.calledWith('modify', { info: 'Modified by post' });
});
複製代碼

咱們爲 commit 方法建立了一個僞實現,並更改了 axios.post 的原始實現。這些僞實現能夠捕獲傳遞給它們的參數,並用咱們要求它們返回的內容進行響應。咱們沒有爲 commit 方法指定返回值,因此它將返回一個空值。axios.post 將返回一個 promise,該 promise 被解析爲帶有 body 屬性的對象。

咱們必須將 sinonChai 做爲一個插件添加到 Chai 中,以便可以對調用簽名進行斷言。這個插件擴展了 Chaito.have.been 屬性和 to.have.been.calledWith 方法。

若是咱們返回一個 Promise,測試函數將變成異步的。Mocha 能夠檢測並等待異步函數完成。在函數內部,咱們等待 onModify 方法完成,而後斷言僞 commit 方法是否被調用並傳入了 post 調用返回的參數。

10. 瀏覽器

從代碼的角度來看,咱們已經測試到了應用程序的各個方面。但有一個問題咱們仍然不能回答:應用程序能夠在瀏覽器中運行嗎?使用 Cypress 編寫的端到端測試能夠告訴咱們答案。

Vue CLI 提供以下功能:啓動應用程序並在瀏覽器中運行 Cypress 測試,而後關閉應用程序。若是你想在 headless 模式下運行 Cypress 測試,你必須將 headless 標記添加到命令中。

describe('New todo', () => {
  it('it should change info', () => {
    cy.visit('/');

    cy.contains('.info', 'Click to modify');

    cy.get('button').click();

    cy.contains('.info', 'Modified by click');
  });
});
複製代碼

上述測試代碼的組織結構與單元測試相同:describe 表明測試分組,it 表明測試運行。全局變量 cy 表示 Cypress 運行器。咱們能夠同步地命令運行程序在瀏覽器中執行什麼操做。

在訪問了主頁(visit)以後,咱們能夠經過 CSS 選擇器訪問頁面中的 HTML。咱們可使用 contains 來斷言元素的內容。頁面交互也是相同的方式:首先,選擇元素(get),而後進行交互(click)。在測試的最後,咱們檢查內容是否更改。

總結

咱們已經介紹完了全部的測試用例,從一個函數的基本單元測試到在實際瀏覽器中運行的端到端測試。在本文中,咱們爲 Vue 應用程序的構建塊(組件、存儲、路由)建立了集成測試,並介紹了 mocking 實現的一些基礎。你能夠在現有的或將來的項目中使用這些技術來避免程序上的 bug。但願本文能下降你們爲 Vue 應用程序編寫測試的門檻。

本文中的示例闡明瞭測試相關的許多事情,但願大家喜歡!


若是你以爲這篇內容對你有價值,請點贊,並關注咱們的官網和咱們的微信公衆號(WecTeam),每週都有優質文章推送:

WecTeam
相關文章
相關標籤/搜索