Web 前端單元測試到底要怎麼寫?看這一篇就夠了

發表於2018-08-16

隨著 Web 應用的複雜程度越來越高,很多公司越來越重視前端單元測試。我們看到的大多數教程都會講單元測試的重要性、一些有代表性的測試框架 api 怎麼使用,但在實際專案中單元測試要怎麼下手?測試用例應該包含哪些具體內容呢?

本文從一個真實的應用場景出發,從設計模式、程式碼結構來分析單元測試應該包含哪些內容,具體測試用例怎麼寫,希望看到的童鞋都能有所收穫。

專案用到的技術框架

該專案採用 react 技術棧,用到的主要框架包括:reactreduxreact-reduxredux-actionsreselectredux-sagaseamless-immutableantd

應用場景介紹

圖片描述

這個應用場景從 UI 層來講主要由兩個部分組成:

  • 工具欄,包含重新整理按鈕、關鍵字搜尋框
  • 表格展示,採用分頁的形式瀏覽

看到這裡有的童鞋可能會說:切!這麼簡單的介面和業務邏輯,還是真實場景嗎,還需要寫神馬單元測試嗎?

別急,為了保證文章的閱讀體驗和長度適中,能講清楚問題的簡潔場景就是好場景不是嗎?慢慢往下看。

設計模式與結構分析

在這個場景設計開發中,我們嚴格遵守 redux 單向資料流 與 react-redux 的最佳實踐,並採用 redux-saga 來處理業務流,reselect 來處理狀態快取,通過 fetch 來呼叫後臺介面,與真實的專案沒有差異。

分層設計與程式碼組織如下所示:
圖片描述

中間 store 中的內容都是 redux 相關的,看名稱應該都能知道意思了。

具體的程式碼請看 這裡

單元測試部分介紹

先講一下用到了哪些測試框架和工具,主要內容包括:

  • jest ,測試框架
  • enzyme ,專測 react ui 層
  • sinon ,具有獨立的 fakes、spies、stubs、mocks 功能庫
  • nock ,模擬 HTTP Server

如果有童鞋對上面這些使用和配置不熟的話,直接看官方文件吧,比任何教程都寫的好。

接下來,我們就開始編寫具體的測試用例程式碼了,下面會針對每個層面給出程式碼片段和解析。那麼我們先從 actions 開始吧。

為使文章儘量簡短、清晰,下面的程式碼片段不是每個檔案的完整內容,完整內容在 這裡

actions

業務裡面我使用了 redux-actions 來產生 action,這裡用工具欄做示例,先看一段業務程式碼:

對於 actions 測試,我們主要是驗證產生的 action 物件是否正確:

這個測試用例的邏輯很簡單,首先構建一個我們期望的結果,然後呼叫業務程式碼,最後驗證業務程式碼的執行結果與期望是否一致。這就是寫測試用例的基本套路。

我們在寫測試用例時儘量保持用例的單一職責,不要覆蓋太多不同的業務範圍。測試用例數量可以有很多個,但每個都不應該很複雜。

reducers

接著是 reducers,依然採用 redux-actionshandleActions 來編寫 reducer,這裡用表格的來做示例:

這裡的狀態物件使用了 seamless-immutable

對於 reducer,我們主要測試兩個方面:

  1. 對於未知的 action.type ,是否能返回當前狀態。
  2. 對於每個業務 type ,是否都返回了經過正確處理的狀態。

下面是針對以上兩點的測試程式碼:

這裡的測試用例邏輯也很簡單,依然是上面斷言期望結果的套路。下面是 selectors 的部分。

selectors

selector 的作用是獲取對應業務的狀態,這裡使用了 reselect 來做快取,防止 state 未改變的情況下重新計算,先看一下表格的 selector 程式碼:

這裡的分頁器部分引數在專案中是統一設定,所以 reselect 很好的完成了這個工作:如果業務狀態不變,直接返回上次的快取。分頁器預設設定如下:

那麼我們的測試也主要是兩個方面:

  1. 對於業務 selector ,是否返回了正確的內容。
  2. 快取功能是否正常。

測試程式碼如下:

測試用例依然很簡單有木有?保持這個節奏就對了。下面來講下稍微有點複雜的地方,sagas 部分。

sagas

這裡我用了 redux-saga 處理業務流,這裡具體也就是非同步呼叫 api 請求資料,處理成功結果和錯誤結果等。

可能有的童鞋覺得搞這麼複雜幹嘛,非同步請求用個 redux-thunk 不就完事了嗎?別急,耐心看完你就明白了。

這裡有必要大概介紹下 redux-saga 的工作方式。saga 是一種 es6 的生成器函式 – Generator ,我們利用他來產生各種宣告式的 effects ,由 redux-saga 引擎來消化處理,推動業務進行。

這裡我們來看看獲取表格資料的業務程式碼:

不熟悉 redux-saga 的童鞋也不要太在意程式碼的具體寫法,看註釋應該能瞭解這個業務的具體步驟:

  1. 從對應的 state 裡取到呼叫 api 時需要的引數部分(搜尋關鍵字、分頁),這裡呼叫了剛才的 selector。
  2. 組合好引數並呼叫對應的 api 層。
  3. 如果正常返回結果,則傳送成功 action 通知 reducer 更新狀態。
  4. 如果錯誤返回,則傳送錯誤 action 通知 reducer。

那麼具體的測試用例應該怎麼寫呢?我們都知道這種業務程式碼涉及到了 api 或其他層的呼叫,如果要寫單元測試必須做一些 mock 之類來防止真正呼叫 api 層,下面我們來看一下 怎麼針對這個 saga 來寫測試用例:

這個測試用例相比前面的複雜了一些,我們先來說下測試 saga 的原理。前面說過 saga 實際上是返回各種宣告式的 effects ,然後由引擎來真正執行。所以我們測試的目的就是要看 effects 的產生是否符合預期。那麼effect 到底是個神馬東西呢?其實就是字面量物件!

我們可以用在業務程式碼同樣的方式來產生這些字面量物件,對於字面量物件的斷言就非常簡單了,並且沒有直接呼叫 api 層,就用不著做 mock 咯!這個測試用例的步驟就是利用生成器函式一步步的產生下一個 effect ,然後斷言比較。

從上面的註釋 3、4 可以看到,redux-saga 還提供了一些輔助函式來方便的處理分支斷點。

這也是我選擇 redux-saga 的原因:強大並且利於測試。

api 和 fetch 工具庫

接下來就是api 層相關的了。前面講過呼叫後臺請求是用的 fetch ,我封裝了兩個方法來簡化呼叫和結果處理:getJSON()postJSON() ,分別對應 GET 、POST 請求。先來看看 api 層程式碼:

業務程式碼很簡單,那麼測試用例也很簡單:

由於 api 層直接呼叫了工具庫,所以這裡用 sinon.stub() 來替換工具庫達到測試目的。

接著就是測試自己封裝的 fetch 工具庫了,這裡 fetch 我是用的 isomorphic-fetch ,所以選擇了 nock 來模擬 Server 進行測試,主要是測試正常訪問返回結果和模擬伺服器異常等,示例片段如下:

基本也沒什麼複雜的,主要注意 fetch 是 promise 返回,jest 的各種非同步測試方案都能很好滿足。

剩下的部分就是跟 UI 相關的了。

容器元件

容器元件的主要目的是傳遞 state 和 actions,看下工具欄的容器元件程式碼:

那麼測試用例的目的也是檢查這些,這裡使用了 redux-mock-store 來模擬 redux 的 store :

很簡單有木有,所以也沒啥可說的了。

UI 元件

這裡以表格元件作為示例,我們將直接來看測試用例是怎麼寫。一般來說 UI 元件我們主要測試以下幾個方面:

  • 是否渲染了正確的 DOM 結構
  • 樣式是否正確
  • 業務邏輯觸發是否正確

下面是測試用例程式碼:

得益於設計分層的合理性,我們很容易利用構造 props 來達到測試目的,結合 enzymesinon ,測試用例依然保持簡單的節奏。

總結

以上就是這個場景完整的測試用例編寫思路和示例程式碼,文中提及的思路方法也完全可以用在 VueAngular 專案上。完整的程式碼內容在 這裡 (重要的事情多說幾遍,各位童鞋覺得好幫忙去給個 哈)。

最後我們可以利用覆蓋率來看下用例的覆蓋程度是否足夠(一般來說不用刻意追求 100%,根據實際情況來定):
圖片描述

單元測試是 TDD 測試驅動開發的基礎。從以上整個過程可以看出,好的設計分層是很容易編寫測試用例的,單元測試不單單只是為了保證程式碼質量:他會逼著你思考程式碼設計的合理性,拒絕麵條程式碼

借用 Clean Code 的結束語:

2005 年,在參加于丹佛舉行的敏捷大會時,Elisabeth Hedrickson 遞給我一條類似 Lance Armstrong 熱銷的那種綠色腕帶。這條腕帶上面寫著“沉迷測試”(Test Obsessed)的字樣。我高興地戴上,並自豪地一直系著。自從 1999 年從 Kent Beck 那兒學到 TDD 以來,我的確迷上了測試驅動開發。

不過跟著就發生了些奇事。我發現自己無法取下腕帶。不僅是因為腕帶很緊,而且那也是條精神上的緊箍咒。那腕帶就是我職業道德的宣告,也是我承諾盡己所能寫出最好程式碼的提示。取下它,彷彿就是違背了這些宣告和承諾似的。

所以它還在我的手腕上。在寫程式碼時,我用餘光瞟見它。它一直提醒我,我做了寫出整潔程式碼的承諾。

相關文章