给 Hooks 写单元测试的两个示例

1,343 阅读3分钟

本文将通过通过编写两个自定义 hook,给大家展示如何给 React 的自定义 Hook 写单元测试。

本文用到的库是 @testing-library/react-hooksvitest

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 的常用用法。希望对你有所帮助。(这篇写得非常的没有深度,以后绝对不发这种文章了)

如果想了解如何配置测试,可以参考 这篇文章