本文将通过通过编写两个自定义 hook,给大家展示如何给 React 的自定义 Hook 写单元测试。
本文用到的库是 @testing-library/react-hooks 和 vitest。
usePrevious
相信大家对这个自定义 hook 肯定不陌生,使用的话就是如下:
const App = () => {
const [count, setCount] = React.useState(0);
const prevCount = usePrevious(count);
return (
<p>
<button onClick={() => setCount(count + 1)}>加一</button>
<p>
现在是: {count}, 之前是: {prevCount}
</p>
</p>
);
};
实现就是:
import { useEffect, useRef } from 'react';
export default function usePreivous<T>(state: T): T | undefined {
const prev = useRef<T>()
useEffect(() => {
prev.current = state
})
return prev.current
}
对于这个,有两个点需要我们测试:
- 初始化的时候,是否为
undefined
- 组件重新渲染后,是否正确使用了上一次的值
import { renderHook } from '@testing-library/react-hooks';
import { expect, it } from 'vitest'
import usePreivous from '../src/usePrevious';
const setup = (value: string) => renderHook(({ name }: { name: string }) => usePreivous(name), {
initialProps: { // 通过 initialProps 作为入参去初始化
name: value
}
})
it('render undefined in initial rendering', () => {
const {result} = setup('0') // result.current 的值就是 usePrevious 的返回值
expect(result.current).toBe(undefined)
})
it('always use preivous value', () => {
const {rerender, result} = setup('0')
rerender({name: '2'}) // rerender 触发重新渲染
expect(result.current).toBe('0')
rerender({name: '4'})
expect(result.current).toBe('2')
})
通过上面这个例子,我们学到了如何去初始化渲染 hook,如何重新渲染我们的 hook。
useHash
这个来获取浏览器当前 url 的哈希值:
const [hash, setHash] = useHash()
实现如下:
import { useEffect, useState, useCallback } from 'react';
export default function useHash() {
const [hash, setHash] = useState(() => window.location.hash)
useEffect(() => {
// hash 还可能由其他地方更改,所以我们要监听 hash 变化的事件
window.addEventListener("hashchange", onHashChange)
function onHashChange() {
setHash(window.location.hash)
}
return () => {
window.removeEventListener('hashChange', onHashChange)
}
}, [])
// 只有在 hash 变化才需要重新生成这个函数
const _setHash = useCallback((newHash: string) => {
if (newHash !== hash) {
setHash(newHash)
}
}, [hash])
return [hash, _setHash] as const
}
对于这个的测试点:
- 我们需要在初始化的时候确定确实返回了当前的 hash
- 在使用
setHash
更改的时候也确实得到了正确的结果 - 当其他地方更改 hash 了,我们组件也能正常监听到响应。
综上所述:
import { act, renderHook } from '@testing-library/react-hooks';
import { test, expect, it, beforeEach } from 'vitest'
import useHash from '../src/useHash'
const setup = () => renderHook(() => useHash())
// 每次更新前初始化
beforeEach(() => {
window.location.hash = '#'
})
it('render with initial hash', () => {
const { result } = setup()
// result.current 格式就是 [hash, setHash]
// 所以,我们取 result.current[0]
expect(result.current[0]).toBe('#')
})
test('return lastest url hash when change hash with setHash ', () => {
const { result } = setup()
expect(result.current[0]).toBe('#')
const [_, setHash] = result.current
// act 能让当前环境更像 React 在浏览器运行的那样
// 我们这里用了 setHash 得这么包一层
act(() => {
setHash('#abc')
})
// 这里直接取值
// 所以 result 设计成 ref 是有原因的 ~
const hash2 = result.current[0];
expect(hash2).toBe('#abc');
})
test('return lastest url hash when change hash with onChangeHash ', () => {
const { result, rerender } = setup()
expect(result.current[0]).toBe('#')
act(() => {
window.location.hash = '#123'
})
const hash2 = result.current[0];
expect(hash2).toBe('#123');
})
执行上面,会发现最后一个执行不通过,为什么呢?这里有一个比较棘手的问题,我们得想办法在测试环境模拟 hashchange
事件,这个没做过可能觉得比较麻烦,事实上是要 mock 一下:
这里没有使用 Jest,而是使用了 Vitest,因为它配置很简单,不过你要是想模拟 DOM 的话,得先配置一下这个:
// filename: vitest.config.ts
import { defineConfig } from 'vitest/config'
export default defineConfig({
test: {
environment: 'happy-dom'
},
})
接着可以完善一下测试用例
import { act, renderHook } from '@testing-library/react-hooks';
import { test, expect, it, beforeEach } from 'vitest'
import useHash from '../src/useHash'
const setup = () => renderHook(() => useHash())
+let hashValue = '#'
+const mochLocation = new Proxy(window.location, {
+ get: (target, p) => {
+ if (p === 'hash') {
+ return hashValue
+ }
+ return Reflect.get(target, p);
+ },
+ set: (target, p, value) => {
+ if (p === 'hash') {
+ hashValue = value
+ window.dispatchEvent(new HashChangeEvent('hashchange'))
+ } else {
+ Reflect.set(target, p, value)
+ }
+
+ return true
+ }
+})
+
+window.location = mochLocation
beforeEach(() => {
window.location.hash = '#'
})
it('render with initial hash', () => {
const { result } = setup()
expect(result.current[0]).toBe('#')
})
// 下面省略
这样,我们就能在测试环境正常触发 hashchange
事件了,上面三个测试用例就都通过了。
通过这个实例,我们学习到了 act
的用法、如何模拟测试环境没有的 API。
上面两个实例就覆盖了 @testing-library/react-hooks 里 renderHook
API 的常用用法。希望对你有所帮助。(这篇写得非常的没有深度,以后绝对不发这种文章了)
如果想了解如何配置测试,可以参考 这篇文章