🎺

react18でtesting-libraryのwrapperにpropsを渡せない件の回避策

2022/10/10に公開

React18になり、testing-libraryreact-hooksがdeprecatedとなった

基本的にはカバーされているのだが、下記のようにwrapperを利用した場合の挙動が変わっていた

const Wrapper = ({children, ...props}) => {
  return <TestContainer {...props}>
    {children}
  </TestContainer>
}

test("Some test", () => {
  const { result,rerender } = renderHook(() => useCounter(),{
    wrapper: Wrapper,
    initialProps: { foo: "baz"}
  })
  rerender({
    foo: "bar"
  })
})

このようなテストを書いた際、以前の@testing-library/react-hooksではwrapperのコンポーネントにpropsが渡されていたが、@testing-library/reactではwrapperはpropsを受け取れない形となっている(そもそも型エラーになる)

それぞれソースとしては下記部分で挙動がちがうことを確認出来る

Contextに依存するようなhooksをテストしたい場合もあり、これはそこそこ困ることがあった

対策

その1: hooks側を対応させる

きれいな解決方法としてはhooks側をtesting-libararyでやりやすいように、値を受け取って処理するhooksとcontextと依存するhooksを分離すること

例えば下記のようなhooksがあった場合


const useMessageCount = () => {
  const messages = useMessageContext()
  const messageCount = useMemo(() => messages.length,[messages])
  const latestMessage = useMemo(() => messages[0], [messages])
  return {
    messageCount,
    latestMessage
  }
}

下記のような分離をする。


const useMessageCount = (messages) => {
  const messageCount = useMemo(() => messages.length,[messages])
  const latestMessage = useMemo(() => messages[0], [messages])
  return {
    messageCount,
    latestMessage
  }
}

const useContextMessageCount = () => {
  const messages = useMessageContext()
  return useMessageCountInternal(messages)
}

こうすればuseMessageCountはこれまで同等のテストが出来るだろう。

その2: renderHookを自前する

hooks自体を書き換えるのがなかなか初手ではやりづらいケースもあるだろう。

幸いtesting-library/react側のrenderHookはそれほど複雑でもないので、あまりきれいな手段ではないがpropsを受け取れるようなrenderHookを自前するというのもある。

概ねこんな具合だ。
他のオプション周りの型などは省略してしまっているのはご了承いただきたい


export function renderHookWithPropsWrapper<Result, Props>(renderCallback: (props: Props) => Result, options: {
  wrapper: React.JSXElementConstructor<{ children: React.ReactElement } & Props>
  initialProps: Props
}) {
  const { initialProps, wrapper: Wrapper, ...restOptions } = options
  const result = React.createRef<Result>()
  function TestComponent({ renderCallbackProps }: { renderCallbackProps: Props }) {
    const pendingResult = renderCallback(renderCallbackProps)

    React.useEffect(() => {
      // @ts-ignore
      result.current = pendingResult
    })

    return null
  }

  const { rerender: baseRerender, unmount } = render(
    <Wrapper {...initialProps} >
      <TestComponent renderCallbackProps={initialProps} />
    </Wrapper>,
    restOptions
  )

  function rerender(rerenderCallbackProps: Props) {
    return baseRerender(
      <Wrapper {...rerenderCallbackProps} >
        <TestComponent renderCallbackProps={rerenderCallbackProps} />
      </Wrapper>
    )
  }

  return { result, rerender, unmount }
}

一部refの使い方が行儀悪くなっていたりはするが、概ねこれで動作するものにはなるようだった。

GitHubで編集を提案

Discussion