Vue 應用單元測試的策略與實踐 02 - 單元測試基礎

呂立青發表於2018-10-30

本文首發於 Vue 應用單元測試的策略與實踐 02 - 單元測試基礎 | 呂立青的部落格

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

歡迎關注我的部落格知乎GitHub掘金


本文的目標

  1. 在 TDD 做完 Tasking 列完例項化資料之後,完全沒有UT基礎不知道該怎麼寫單元測試?
// Given
一個完全沒有UT基礎的新人?
// When
當他?閱讀和練習本文的Jest的部分
// Then
他能夠把Given/When/Then的套路學會
他能夠學會Jest的基本用法,包括測試suite和斷言等語法
他能夠學會Jest中測試非同步的幾種方式
複製程式碼

單元測試基礎

上一篇文章當中我們介紹了單元測試的意義,以及為何選擇 Facebook 的 Jest 作為我們的測試框架。現在就讓我們一起來學習如何編寫最基礎的單元測試。

如果你已經有了使用 Jest 編寫單元測試的經驗,可以選擇直接跳到第二段。

第一個 Jest 例項

首先建立 jest-demo 專案並安裝 jest 作為專案 devDependencies 依賴:

mkdir jest-demo && cd $_
yarn init -y #--yes
yarn add jest -D #--dev
複製程式碼

然後建立一個 math.js 檔案,輸入一個我們稍後測試的 sum 函式:

const sum = (a, b) => a + b

module.exports = { sum }
複製程式碼

接下來,讓我們寫第一個測試。在同一個資料夾中建立一個 math.test.js 檔案,在這裡我們將使用 Jest 來測試 math.js 中定義的函式:

const { sum } = require('./math')

describe('Math module', () => {
  it("should return sum result when one number plus another number", () => {
    // Given
    const number = 1
    const anotherNumber = 2
    // When
    const result = sum(number, anotherNumber)
    // Then
    expect(result).toBe(2)
  })
})
複製程式碼

然後執行 yarn test (新增 NPM Script)你就可以看到相應的結果。

Vue 應用單元測試的策略與實踐 02 - 單元測試基礎

Given/When/Then 的套路

麻雀雖小五臟俱全,在上面的例子當中,我們可以看到很多的測試元素,下面將會一一介紹:

首先我們看到的是一個由 it 包裹的測試主體最小單元,採用了Given When Then的經典格式,我們常常稱之為測試三部曲,也可以解釋為 3A 即:

GWT 3A 說明
Given Arrange 準備測試測試資料,有時可以抽取到 beforeEach
When Act 採取行動,一般來說就是呼叫相應的模組執行對應的函式或方法
Then Assert 斷言,這時需要藉助的就是Matchers的能力,Jest還可以擴充套件自己的Matcher

expect 後面的 toBe稱之為 Matcher,是斷言時的判斷語句以驗證正確性 ✅,在後面的文章中我們還會接觸更多 Matchers,甚至可以擴充套件一些特別定製的 Matchers。

expect(1+1).toBe(2)
expect(1+1).not.toBe(3)
複製程式碼

修改斷言的結果,就可以看到成功後的結果了:

Vue 應用單元測試的策略與實踐 02 - 單元測試基礎

模組間依賴 Fake/Stub/Mock/Spy

Vue 應用單元測試的策略與實踐 02 - 單元測試基礎

如同人類世界中的羈絆,軟體模組之間必然也免不了依賴。Martin FowlerUnitTest 這篇文章當中將單元測試作了一個重要的區分,即你所測試的單位應該是社交型(Social Tests)還是獨立型(Solitary Tests)? 想象一下你正在測試一個 Order Class 的 price() 方法,而 price() 方法需要在 ProductCustomer Class 中呼叫一些函式。如果你希望單元測試所測試的 Order 模組是獨立的,那麼你就不想直接使用真正的 ProductCustomer Class,因為 Customer Class 的錯誤會直接導致 Order Class 的單元測試失敗。相反,你可能會使用一個替身作為依賴的物件,也就是我們接下來會提到的 Fake/Stub/Mock/Spy。

現實世界裡,我們在寫程式碼和單元測試時,常常遇到的一些需要替身的物件包括:

  • Database 資料庫
  • Network requests 網路請求
  • access to Files 存取檔案
  • any External system 任何外部系統

其實在 Jest 當中,Fake/Stub/Mock/Spy 這些概念或許會有所混淆,而這跟 JavaScript 語言本身的特點有一定關係,但是我覺得 Jest 通過統一的 fn() 方法把問題解決得還比較恰當,讓我們來一塊兒看看例項?:

Mock 用於替代整個模組

import SoundPlayer from './sound-player';

const mockPlaySoundFile = jest.fn();

jest.mock('./sound-player', () => {
  return jest.fn().mockImplementation(() => {
    return {playSoundFile: mockPlaySoundFile};
  });
});
複製程式碼

我們可以看到 jest.mock() 方法中的第二個引數是一個函式,那麼我們就可以完全接管整個 ./sound-player JavaScript 模組,比如說這裡的 playSoundFile 本來應該是從 ./sound-player 這個檔案當中 export 出來的,而被 Mock 之後我們的測試就可以使用 Mock 所返回的資料或方法,從而保證模組所返回的內容是我們所期望的。但這時需要注意的是,該模板的所有功能都已經被 Mock 掉,而不會再從原模組當中返回,所以我們就需要重新實現該模組中的所有功能。可別一不小心就成了張藝謀導演《影》片中的影子,被完全“取而代之”,連夫人也被 Mock 所吸引。

Stub 用於模擬特定行為

const mockFn = jest.fn();
mockFn();
expect(mockFn).toHaveBeenCalled();

// With a mock implementation:
const returnsTrue = jest.fn(() => true);
console.log(returnsTrue()); // true;
複製程式碼

這裡的特定行為也可以是沒有行為,jest.fn() 代表著我就是一個 Stub(樁),“你來我就在這裡,你走我也依然在這裡,風雨無阻”。不需要什麼輸入輸出,只要能在測試的時候驗證到 Stub 被呼叫過就行,也就能夠斷言到某處程式碼被執行,從而確定程式碼被測試所覆蓋。而另一種特定行為就是返回特定的資料,即 Stub 也可以根據輸入模擬返回一種輸出,作為某些模組的替身幫它演戲,比如“小鮮肉們”遇到要跳車啦、要卿卿我我(誤)的時候就要找替身,“一二三四五六七八”連臺詞都不用背還需要配音。

Spy 用於監聽模組行為

Spy packages without affecting the functions code

const video = require('./video');

it('plays video', () => {
  const spy = jest.spyOn(video, 'play');
  const isPlaying = video.play();

  expect(spy).toHaveBeenCalled();
  expect(isPlaying).toBe(true);
})
複製程式碼

Spy 並不會影響到原有模組的功能程式碼,而只是充當一個監護人的作用,“你可以繼續我型我秀上課講小話,但是老師會偷偷告訴你媽媽,看你放學後老媽不打斷你的腿”。比如說上文中的 video 模組中的 play() 方法已經被 spy 過,那麼之後 play() 方法只要被呼叫過,我們就能判斷其是否執行,甚至執行的次數。

如何 Mock 全域性的方法?

把全域性的資料 Mock 掉很簡單,只需要像 window.document.title = undefined 這樣簡單 Fake 賦值就很完美。而像 matchMedia 這樣的方法在 jsdom 裡面並沒有被實現,這時候我們當然就需要去把它 Mock 掉,簡單把要用到的一些物件屬性賦值就好,總之不至於在執行時報錯。

window.matchMedia = jest.fn().mockImplementation(query => {
  return {
    matches: false,
    media: query,
    onchange: null,
    addListener: jest.fn(),
    removeListener: jest.fn(),
  };
});
複製程式碼

程式碼模組的易測性

從上文的一些例子當中,我們也可以看到,不管是 Fake/Stub/Mock/Spy 最最重要的一個原則就是「簡單」,因為我們是在寫測試程式碼,而所依賴的模組就應該以最簡單的形態展現出來,絕不要給 jest.fn() 編寫過於哪怕一點點複雜的邏輯。如果這個模組有多種表現形態,那就把它分種測試單元進行多次 Mock,每個 it() 單元測試一定是針對於單個功能點進行測試的。

保持單元測試獨立性的同時,也是在促使你去思考什麼樣的模組才是符合「職責單一原則」的。單元測試站在使用者的角度來使用該模組,而程式碼的易測性也就代表著程式碼的可維護性。

如何測試非同步程式碼?

非同步是 JavaScript 中繞不開的永恆話題,多虧了 ES6+ 高階語法所提供的多種優雅的非同步程式碼方式,讓我們寫測試程式碼的方式也多了好多種。(逃

讓我們先來看一下什麼是非同步請求,這裡有一個通過 Chrome API 獲取當前位置的例項,可想而知 Chrome 要根據 GPS 訊號才能算出當前的經緯度,相當於從衛星?來回走了一遭,怎麼不會非同步(代表有延時,延遲返回)呢?

navigator.geolocation.getCurrentPostion() # chrome API 非同步獲取當前位置
複製程式碼

Callback 回撥函式

it('the data is peanut butter', done => {
  function callback(data) {
    expect(data).toBe('peanut butter');
    done();
  }

  fetchData(callback);
});
複製程式碼

這是最最普通的方式,也是各大框架都支援的一種寫法, done() 作為非同步程式碼結束的結束標誌,從而讓測試框架“知道”在結束時進行斷言。但這種方式侵入性比較強,對測試語句不友好且違背了 Given/When/Then 的三段式套路,就像回撥地獄一樣的道理,如果讓 done() 充斥著測試那麼程式碼也就變得混亂。

Promise 讓愛 then() 到底

it('the data is peanut butter', () => {
  expect.assertions(1);
  return fetchData().then(data => {
    expect(data).toBe('peanut butter');
  });
});
複製程式碼
expect(Promise.resolve('lemon')).resolves.toBe('lemon')

expect(Promise.reject(new Error('octopus'))).rejects.toThrow('octopus')
複製程式碼

其實這種方式也好不到哪去,無非就是把 done() 方式換成了 then() 又一次充斥在整個 expect 當中,混亂了 When 和 Then 兩種本該分開的時刻。但也有一個不錯的點,可以通過 Promise 的 .resolve().reject() 方法使測試分別驗證正常或異常的情況。

Async/Await 讓非同步變得同步

test('the data is peanut butter', async () => {
  expect.assertions(1);
  const data = await fetchData();
  expect(data).toBe('peanut butter');
});
複製程式碼

Async/Await 語法糖在業務程式碼當中就特別好使了,好處不多說直接看得見:原本需要 done()then() 的地方都不再混亂,又一次迴歸到了正常的 Given/When/Then 三段式套路,讓測試程式碼變得非常清晰易讀。唯一需要注意的是, 額外的expect.assertions(number) 其實是驗證在測試期間所呼叫的斷言數量,這在測試多層非同步程式碼時很有用,以確保實際呼叫回撥中的斷言次數。

意猶未盡嗎?更加Jest相關的內容可以檢視這篇文章 Testing JavaScript with Jest,與此同時具體的 API 可以參考官方文件

未完待續……

## 單元測試基礎

  • [x] ### 單元測試與自動化的意義
  • [x] ### 為什麼選擇 Jest
  • [x] ### Jest 的基本用法
  • [x] ### 該如何測試非同步程式碼?

## Vue 單元測試

  • [ ] ### Vue 元件的渲染方式
  • [ ] ### Wrapper find() 方法與選擇器
  • [ ] ### UI 元件互動行為的測試

## Vuex 單元測試

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

## Vue應用測試策略

  • [ ] ### 單元測試的特點及其位置
  • [ ] ### 單元測試的關注點
  • [ ] ### 應用測試的測試策略

相關文章