Vue 應用單元測試的策略與實踐 02 - 單元測試基礎
本文的目標
- 在 TDD 做完 Tasking 列完例項化資料之後,完全沒有UT基礎不知道該怎麼寫單元測試?
// Given 一個完全沒有UT基礎的新人:walking: // When 當他:walking:閱讀和練習本文的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('Match 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
你就可以看到相應的結果。
Given/When/Then 的套路
麻雀雖小五臟俱全,在上面的例子當中,我們可以看到很多的測試元素,下面將會一一介紹:
首先我們看到的是一個由 it
包裹的測試主體最小單元,採用了Given When Then的經典格式,我們常常稱之為測試三部曲,也可以解釋為 3A 即:
GWT | 3A | 說明 |
---|---|---|
Given | Arrange | 準備測試測試資料,有時可以抽取到 beforeEach |
When | Act | 採取行動,一般來說就是呼叫相應的模組執行對應的函式或方法 |
Then | Assert | 斷言,這時需要藉助的就是Matchers的能力,Jest還可以擴充套件自己的Matcher |
在 expect
後面的 toBe
稱之為 Matcher,是斷言時的判斷語句以驗證正確性 :white_check_mark:,在後面的文章中我們還會接觸更多 Matchers,甚至可以擴充套件一些特別定製的 Matchers。
expect(1+1).toBe(2) expect(1+1).not.toBe(3)
修改斷言的結果,就可以看到成功後的結果了:
模組間依賴 Fake/Stub/Mock/Spy
如同人類世界中的羈絆,軟體模組之間必然也免不了依賴。 ofollow,noindex" target="_blank">Martin Fowler 在 UnitTest 這篇文章當中將單元測試作了一個重要的區分,即你所測試的單位應該是社交型(Social Tests)還是獨立型(Solitary Tests)? 想象一下你正在測試一個 Order
Class 的 price()
方法,而 price()
方法需要在 Product
和 Customer
Class 中呼叫一些函式。如果你希望單元測試所測試的 Order
模組是獨立的,那麼你就不想直接使用真正的 Product
或 Customer
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()
方法把問題解決得還比較恰當,讓我們來一塊兒看看例項:chestnut::
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()
編寫
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 可以參考 官方文件 。
未完待續……
## 單元測試基礎
- ### 單元測試與自動化的意義
- ### 為什麼選擇 Jest
- ### Jest 的基本用法
- ### 該如何測試非同步程式碼?
## Vue 單元測試
- ### Vue 元件的渲染方式
- ### Wrapper
find()
方法與選擇器 - ### UI 元件互動行為的測試
## Vuex 單元測試
- ### CQRS 與
Redux-like
架構 - ### 如何對 Vuex 進行單元測試
- ### Vue元件和Vuex store的互動