如何對(duì)react hooks進(jìn)行單元測(cè)試的方法
寫在前面
使用 react hook 來(lái)做公司的新項(xiàng)目有一段時(shí)間了,大大小小的坑踩了不少。由于是公司項(xiàng)目,因此必須要編寫單元測(cè)試來(lái)確保業(yè)務(wù)邏輯的正確性以及重構(gòu)時(shí)代碼的可維護(hù)性與穩(wěn)定性,之前的項(xiàng)目使用的是 react@15.x 的版本,使用 enzyme 配合 jest 來(lái)做單元測(cè)試毫無(wú)壓力,但新項(xiàng)目使用的是 react@16.8 ,編寫單元測(cè)試的時(shí)候,遇到不少阻礙,因此總結(jié)此篇文章算作心得分享出來(lái)。
配合 enzyme 來(lái)進(jìn)行測(cè)試
首先,enzyme 對(duì)于 hook 的支持程度,可以參考這個(gè) issue,對(duì)于各個(gè) hook 的支持程度,里面有鏈接,有說(shuō)明,這里就不贅述了。我在這里想說(shuō)的是,使用 enzyme 來(lái)測(cè)試 hook 在測(cè)試以及驗(yàn)證方式上的一些轉(zhuǎn)變。
測(cè)試狀態(tài)
由于 function component 沒有實(shí)例的概念,我們無(wú)法通過(guò)類似 instance.xxx 的方式來(lái)直接對(duì)狀態(tài)進(jìn)行驗(yàn)證,比如:
對(duì)于這里的 count 是無(wú)法通過(guò) enzyme 中 wrapper.state 的 api 來(lái)訪問的,但是我們可以通過(guò) wrapper.text 來(lái)取出 button 的文字節(jié)點(diǎn),間接地測(cè)試 count 狀態(tài),如:
const Counter = () => { const [count, setCount] = useState(0) return <button>{count}</button> }
測(cè)試方法
同理,我們也無(wú)法通過(guò) instance.methodXXX 的方式來(lái)直接獲取組件實(shí)例的方法,進(jìn)而進(jìn)行調(diào)用和測(cè)試,比如:
const wrapper = mount(<Counter/>) expect(wrapper.find('button').text()).toBe('0')
如何獲取 inc 方法的引用呢?我們可以通過(guò) wrapper.prop 來(lái)曲線救國(guó):
const Counter = () => { const [count, setCount] = useState(0) const inc = useCallback(() => setCount(c => c + 1), []) return <button onClick={inc}>{count}</button> }
另外,有些情況下,我們以返回值的方式來(lái)暴露 hook 中的一些狀態(tài)以及方法,如果是這樣的話,就更簡(jiǎn)單了,可以通過(guò)編寫 Wrapper 組件或者直接使用下一小節(jié)提及的工具庫(kù)來(lái)進(jìn)行測(cè)試。
使用 @testing-library/react-hooks
測(cè)試有返回值的 hook
關(guān)于這個(gè)工具庫(kù),在它的代碼倉(cāng)庫(kù)中的 README.md 對(duì)它要解決的問題、實(shí)現(xiàn)原理進(jìn)行了詳細(xì)的說(shuō)明,有興趣的甚至可以直接看它的源碼,十分簡(jiǎn)單。這里給出一個(gè)示例來(lái)演示如何測(cè)試上一小節(jié)最后所說(shuō)的情況,比如我們有一個(gè) hook:
function useCounter() { const [count, setCount] = useState(0) const inc = useCallback(() => setCount(c => c + 1), []) const dec = useCallback(() => setCount(c => c - 1), []) return { count, inc, dec } }
首先,我們完全可以通過(guò)上一小節(jié)的方式來(lái)對(duì)它進(jìn)行測(cè)試,只需要實(shí)現(xiàn)一個(gè)臨時(shí)的 Wrapper,比如:
const CounterIncWrapper = () => { const {count, inc} = useCounter() return <button onClick={inc}>{count}</button> } const CounterDecWrapper = () => { const {count, dec} = useCounter() return <button onClick={dec}>{count}</button> }
然后單獨(dú)按照上一節(jié)提及的方式來(lái)測(cè)試 CounterIncWrapper 或者 CounterDecWrapper 就可以了,但我們會(huì)發(fā)現(xiàn),這里的 Wrapper 的邏輯是很相似的,我們是否可以將它抽離為一個(gè)公用的邏輯呢?答案當(dāng)然是可以的,這正是 @testing-library/react-hooks 做的,使用它我們可以這樣測(cè)試 hook ,如下:
test('should increment counter', () => { const { result } = renderHook(() => useCounter()) act(() => { result.current.inc() }) expect(result.current.count).toBe(1) act(() => { result.current.dec() }) expect(result.current.count).toBe(0) })
這里的 act 是內(nèi)置的工具方法,可以參考官方文檔進(jìn)行了解,任何對(duì)于狀態(tài)的修改,都應(yīng)該在它的回調(diào)函數(shù)中進(jìn)行,不然會(huì)出現(xiàn)錯(cuò)誤警告。
測(cè)試有依賴項(xiàng)的 hook
有些情況下,我們的 hook 會(huì)存在依賴的,比較常見的是 useContext 這個(gè) hook ,它依賴一個(gè) Provider 父組件,比如輕量級(jí)的狀態(tài)管理庫(kù) unstated-next ,假設(shè)我們將上面的 hook 抽象成了一個(gè)獨(dú)立的 Container (這里會(huì)涉及 unstated-next 的 api ,但不影響理解):
const Counter = createContainer(useCounter)
要使用這個(gè) Container ,我們需要這樣:
可以發(fā)現(xiàn),這里的 CounterDisplay 依賴于 Counter.Provider ,要測(cè)試 CounterDisplay ,我們通過(guò) renderHook 的 wrapper 參數(shù)來(lái)注入父組件,比如:
function CounterDisplay() { let counter = Counter.useContainer() return ( <div> <button onClick={counter.dec}>-</button> <span>{counter.count}</span> <button onClick={counter.inc}>+</button> </div> ) } function App() { return ( <Counter.Provider> <CounterDisplay /> </Counter.Provider> ) }
另外, renderHook 還支持 initialProps 參數(shù),它代表回調(diào)函數(shù)中的參數(shù),這里接不贅述了。
測(cè)試副作用
hook 中比較難搞的應(yīng)該算是 useEffect ,我花了很長(zhǎng)時(shí)間來(lái)看別人是如何對(duì)它進(jìn)行單元測(cè)試的,但是并沒有得到一些有用的信息,后來(lái)我仔細(xì)想了想,其實(shí)這個(gè)問題應(yīng)該這樣來(lái)想, useEffect 是用來(lái)封裝副作用的,它只用來(lái)負(fù)責(zé)副作用的運(yùn)行時(shí)機(jī),對(duì)于副作用干了什么,對(duì)于 useEffect 完全是透明的。因此我們沒有必要對(duì)它進(jìn)行單元測(cè)試,而應(yīng)該在副作用的實(shí)現(xiàn)層確保它的正確性。但我們通常會(huì)將副作用的實(shí)現(xiàn)與 hook 的實(shí)現(xiàn)耦合起來(lái),那怎么對(duì)副作用的實(shí)現(xiàn)進(jìn)行測(cè)試呢?這里可以分兩種情況。
useEffect 會(huì)運(yùn)行 props 中傳遞的回調(diào)函數(shù)
這種情況相對(duì)簡(jiǎn)單一些,只需要通過(guò) jest.fn() 來(lái)構(gòu)造一個(gè) spy 函數(shù),之后通過(guò)上一節(jié)的方式渲染 hook ,通過(guò) jest 對(duì)于 spy 函數(shù)的 api 來(lái)進(jìn)行驗(yàn)證即可。
useEffect 自成一體
這種情況下,我當(dāng)前是通過(guò)將副作用代碼,直接聲明在 hook 外部的方式來(lái)進(jìn)行測(cè)試的,比如:
export function updateDocumentTitle(title) { document.title = title return () => { document.title = 'default title' } } export function useDocumentTitle(title) { useEffect(() => updateDocumentTitle(title), [title]) }
這樣,只需要單獨(dú)測(cè)試 updateDocumentTitle 就好,而不需要在 useEffect 上花費(fèi)功夫了。
這里可能有的人會(huì)問,你這里無(wú)法覆蓋 title 改變時(shí), effect 是否重新運(yùn)行的場(chǎng)景,確實(shí),當(dāng)前我也沒有辦法解決這種問題,如果要解決,辦法還是有的,就是通過(guò) useDocumentTitle 的參數(shù),來(lái)傳遞 updateDocumentTitle ,但這對(duì)于代碼有很強(qiáng)的侵入性,我不建議這樣做,如果 hook 本身的實(shí)現(xiàn)方式就是這樣,那完全可以針對(duì)它編寫相關(guān)的測(cè)試用例,如果不是,也沒有必要為了寫測(cè)試用例而改寫原來(lái)的實(shí)現(xiàn)。
hook 無(wú)法被測(cè)試的原因
在對(duì)公司項(xiàng)目各個(gè) hook 編寫單元測(cè)試時(shí),發(fā)現(xiàn)一些 hook 非常難以測(cè)試,大體的特征如下:
- hook 的實(shí)現(xiàn)非常復(fù)雜,狀態(tài)繁多,依賴繁多
- hook 的實(shí)現(xiàn)不復(fù)雜,但外部依賴難以 mock
- hook 的實(shí)現(xiàn)自成一體,沒有入口
關(guān)于第一點(diǎn),解決的方法當(dāng)然是,化繁為簡(jiǎn),將復(fù)雜的 hook,劃分為多個(gè)簡(jiǎn)單的 hook,使其職責(zé)更單一。對(duì)于第二點(diǎn),如果外部依賴難以 mock ,我建議將它的測(cè)試用例放到集成測(cè)試階段進(jìn)行實(shí)現(xiàn),而不要花費(fèi)過(guò)多精力在編寫單元測(cè)試的 mock 邏輯上。最后一點(diǎn)的解決方法詳見上一小節(jié)。
以上就是本文的全部?jī)?nèi)容,希望對(duì)大家的學(xué)習(xí)有所幫助,也希望大家多多支持腳本之家。
相關(guān)文章
React?Native項(xiàng)目設(shè)置路徑別名示例
這篇文章主要為大家介紹了React?Native項(xiàng)目設(shè)置路徑別名實(shí)現(xiàn)示例詳解,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步,早日升職加薪2023-05-05React項(xiàng)目中decorators裝飾器報(bào)錯(cuò)問題解決方案
這篇文章主要介紹了React項(xiàng)目中decorators裝飾器報(bào)錯(cuò),本文給大家分享問題所在原因及解決方案,通過(guò)圖文實(shí)例相結(jié)合給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-01-01react函數(shù)組件useState異步,數(shù)據(jù)不能及時(shí)獲取到的問題
這篇文章主要介紹了react函數(shù)組件useState異步,數(shù)據(jù)不能及時(shí)獲取到的問題,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-08-08React和Vue中實(shí)現(xiàn)錨點(diǎn)定位功能
在React中,可以使用useState和useEffect鉤子來(lái)實(shí)現(xiàn)錨點(diǎn)定位功能,在Vue中,可以使用指令來(lái)實(shí)現(xiàn)錨點(diǎn)定位功能,在React和Vue中實(shí)現(xiàn)錨點(diǎn)定位功能的方法略有不同,下面我將分別介紹,文中通過(guò)代碼示例介紹的非常詳細(xì),需要的朋友可以參考下2024-01-01詳解如何在React單頁(yè)面應(yīng)用中捕獲錯(cuò)誤
在當(dāng)前的Web開發(fā)中,使用React構(gòu)建單頁(yè)面應(yīng)用(SPA)已經(jīng)成為一種常見的做法,然而,當(dāng)應(yīng)用程序遇到錯(cuò)誤時(shí),有可能會(huì)導(dǎo)致整個(gè)頁(yè)面崩潰,給用戶帶來(lái)不好的體驗(yàn),本文將介紹如何在React單頁(yè)面應(yīng)用中捕獲錯(cuò)誤,以防止整個(gè)頁(yè)面的崩潰,需要的朋友可以參考下2023-09-09React Native:react-native-code-push報(bào)錯(cuò)的解決
這篇文章主要介紹了React Native:react-native-code-push報(bào)錯(cuò)的解決,具有很好的參考價(jià)值,希望對(duì)大家有所幫助。如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2022-10-10