小弟團隊開發的前端專案是使用Context API與Hooks自幹的Flux-like架構。前陣子替產品的critical path測試flow遇到了一些障礙,主要是遇到透過reducer更新context state資料的部分難以測試,但這又是出了問題會半夜叫你起床尿尿的部分,實在是不得不寫。最後土炮出來決定寫篇文章做筆記順便紀念一下。以下使用常遇見的計數器來當example。
先從Flux-like架構開始吧!
如果想直接看code可以進入這個repo
首先我們使用createContext API將default state包裝成Context物件,state object只有一個count property記錄目前數字,接著在最外層的component使用useContext API帶入。
Counter使用的reducer很單純,只有加一減一的功能。我們使用useReducer API產生dispatch,最後透過Provider API將state 與dispatch傳給子層component使用。
其實對實作Counter來說這樣的架構實在過於複雜,只是為了demo使用。在production code如果只是實作同樣簡單的邏輯或許在component內做掉就好囉
Counter元件
接著來看看Counter元件。在團隊及個人開發的慣例都是習慣寫HOC將元件切分成Presentation與Container Component把邏輯與UI拆開,好處為可以將邏輯給不同UI復用等等,不過這不是這篇文章的重點就不贅述了。
我們在Container使用useContext API將Provider傳下的state與dispatch解構出來後將加一減一的dispatch包成function,並與目前數字包在props內作為Presentation可以使用的API。
接著來撰寫測試吧
在這例子中我想要測試Counter Container的increment與decrement function是否可以正確的dispatch給reducer,並且讓Context裡的count數字做增減。由於常常遇到各種test case需要給不同的input測試各種情境,例如我希望測試Counter的increment數字可以從 0 -> 1,decrement 從2->1。因此我們可以試著將default state的count做參數化。
在沒有產品Domain的情境下這兩個test case看起來有點牽強,實在是想不太到更好的例子就不要太介意了😂
這裡我們試著將count的數字參數化放入Context看看吧,既然要動態帶入state就必須要mock Context模組才行。我們可以使用 jest.doMock API將Context mock成我們自定義state與dispatch。與jest.mock不同的是doMock會避免Hoisted的發生,就可以在執行環境的execution phase各個test case帶入不同的參數,想了解執行環境與Hoisted的人可以參考小弟之前寫的這篇文章
這裡可以注意到mockReducer不能透過React useReducer API產生,原因是因為Hook本身不能在Component外使用,使用會得到Invalid hook call的Error,非常討厭。但由於reducer的本質是將state帶入操作後吐回去新的state物件,基本上像第7行的寫法就可以了。
終於可以開始測試Component本身了!由於內容簡單就直接貼上testcase程式碼。不過雖然看似簡單但撰寫的過程還是踩雷,以下大略介紹邏輯與踩到的雷包
首先是Container需要以dynamic import的方式載入才能讓component吃到mock過的Context。由於將UI與邏輯拆分的關係,使用的mock component就只要帶入Fragment當作假UI就好了。最後再使用Test Renderer將component render並手動invoke從props傳遞給UI的increment API,最後做Assertion。
以上的邏輯與概念都很簡單,但最後執行測試時卻遇到了jest的雷包。如果有點進去看jest.doMock的官網文件會發現範例寫法會在每個testcase前做reset module的動作,為了將每個mock module做重置
beforeEach(() => { jest.resetModules(); });
但萬萬沒想到執行這段會讓使用hook的component噴發令人崩潰的Invalid hook call Error。大概查了一下目前的workaround是使用 jest.isolateModules將各個mock module隔離,有興趣深入了解的話可以參考這篇討論
人生有多少時間可以寫這樣麻煩的測試?
到這邊應該會發現撰寫這樣的測試似乎時間成本有點太高,即使只是測加一減一的簡單邏輯。小弟個人認為替產品撰寫測試可能還是要有所取捨,如果在production code遇到像Counter這樣簡單且不屬於critical path的話,我可能會選擇單純對reducer做單元測試即可,人生還是該把時間花在更有意義的事情上。
最後分享Github上的完整程式碼,這個範例即使我先前已經寫過類似的測試也花了不少時間在解遇到的雷包以及其他小問題,希望可以幫助到被測試偷走時間的人,也同時希望如果大神們發現我繞了遠路或是有問題的地方也可以指點迷津,非常感謝!