【问题标题】:Testing async custom hooks that useEffect with react-hooks-testing-library使用 react-hooks-testing-library 测试 useEffect 的异步自定义钩子
【发布时间】:2021-10-22 17:05:03
【问题描述】:

我写了一个简单的 React 钩子,想用 react-hooks-testing-library 测试它。

这个钩子在providerdomain 变量都调用一次异步函数,然后一旦它被解析,就会将数据放入状态并返回它。

我在任何地方都找不到如何测试异步挂钩,但发现了这个:How to test custom async/await hook with react-hooks-testing-library。这个问题没有答案,但至少我发现我需要打电话给waitForNextUpdate 来测试useEffect

我的测试的问题是它只运行一次钩子(在挂载时),而不是在设置提供者和域时。所以它总是挂在默认值上。如何让它重新触发钩子以实际调用getENS 并设置数据?

下面是钩子的样子:

import { useEffect, useState } from 'react'
import { getENS, ResolvedENS } from 'get-ens'
import type { Provider } from '@ethersproject/providers'

const ac = new AbortController()

export const useENS = (provider: Provider, domain: string): ResolvedENS => {
  const [data, set] = useState<ResolvedENS>({ address: null, owner: null, records: { web: {} } })

  useEffect(() => {
    const load = async () => {
      const data = await getENS(provider)(domain, { signal: ac.signal })

      set(data)
    }

    if (provider && domain) load()

    return () => ac.abort()
  }, [domain, provider])

  return data
}

这就是我的测试的样子:

import { describe, it, jest } from '@jest/globals'
import { renderHook } from '@testing-library/react-hooks'
import { useENS } from '../packages/use-ens/src/index'
import { providers } from 'ethers'
import fetch from 'fetch-mock'

jest.setTimeout(30000)

describe('use-ens', () => {
  it('should return resolved data for a domain', async () => {
    fetch.mock('*', {
      data: {
        domains: [
          {
            resolvedAddress: { id: '0xf75ed978170dfa5ee3d71d95979a34c91cd7042e' },
            resolver: {
              texts: ['avatar', 'color', 'description', 'email', 'url', 'com.github', 'com.instagram', 'com.twitter']
            },
            owner: { id: '0xf75ed978170dfa5ee3d71d95979a34c91cd7042e' }
          }
        ]
      }
    })

    const provider = new providers.InfuraProvider('homestead', 'INFURA_API_KEY')
    const { result, waitForNextUpdate } = renderHook<[providers.BaseProvider, string], ReturnType<typeof useENS>>(() =>
      useENS(provider, 'foda.eth')
    )

    console.log(result.current)

    await waitForNextUpdate()

    console.log(result.current)
  })
})

当我运行测试时出现此错误:

> NODE_OPTIONS=--experimental-vm-modules pnpx jest tests
  console.log
    { address: null, owner: null, records: { web: {} } }

      at Object.<anonymous> (tests/use-ens.test.ts:30:13)

 FAIL  tests/use-ens.test.ts
  use-ens
    ✕ should return resolved data for a domain (1049 ms)

  ● use-ens › should return resolved data for a domain

    Timed out in waitForNextUpdate after 1000ms.

      30 |     console.log(result.current)
      31 |
    > 32 |     await waitForNextUpdate()
         |     ^
      33 |
      34 |     console.log(result.current)
      35 |   })

      at waitForNextUpdate (node_modules/.pnpm/@testing-library+react-hooks@7.0.1_react-dom@17.0.2+react@17.0.2/node_modules/@testing-library/react-hooks/lib/core/asyncUtils.js:102:13)
      at Object.<anonymous> (tests/use-ens.test.ts:32:5)

Test Suites: 1 failed, 1 total
Tests:       1 failed, 1 total
Snapshots:   0 total
Time:        5.215 s
Ran all test suites matching /tests/i.
Jest did not exit one second after the test run has completed.

This usually means that there are asynchronous operations that weren't stopped in your tests. Consider running Jest with `--detectOpenHandles` to troubleshoot this issue.
  console.error
    Warning: An update to TestComponent inside a test was not wrapped in act(...).
    
    When testing, code that causes React state updates should be wrapped into act(...):
    
    act(() => {
      /* fire events that update state */
    });
    /* assert on the output */
    
    This ensures that you're testing the behavior the user would see in the browser. Learn more at https://reactjs.org/link/wrap-tests-with-act
        at TestComponent (/home/v1rtl/Coding/proj/node_modules/.pnpm/@testing-library+react-hooks@7.0.1_react-dom@17.0.2+react@17.0.2/node_modules/@testing-library/react-hooks/lib/helpers/createTestHarness.js:22:5)

      13 | export const useENS = (provider: Provider, domain: string): ResolvedENS => {
      14 |   const [data, set] = useState<ResolvedENS>({ address: null, owner: null, records: { web: {} } })
    > 15 |
         | ^
      16 |   useEffect(() => {
      17 |     const load = async () => {
      18 |       const data = await getENS(provider)(domain, { signal: ac.signal })

  console.error
    Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
        at TestComponent (/home/v1rtl/Coding/proj/node_modules/.pnpm/@testing-library+react-hooks@7.0.1_react-dom@17.0.2+react@17.0.2/node_modules/@testing-library/react-hooks/lib/helpers/createTestHarness.js:22:5)

      13 | export const useENS = (provider: Provider, domain: string): ResolvedENS => {
      14 |   const [data, set] = useState<ResolvedENS>({ address: null, owner: null, records: { web: {} } })
    > 15 |
         | ^
      16 |   useEffect(() => {
      17 |     const load = async () => {
      18 |       const data = await getENS(provider)(domain, { signal: ac.signal })

      at printWarning (node_modules/.pnpm/react-dom@17.0.2_react@17.0.2/node_modules/react-dom/cjs/react-dom.development.js:67:30)

 ERROR  Test failed. See above for more details.

我还创建了一个包含 Jest 配置文件的 gist:https://gist.github.com/talentlessguy/aad20875b1e4406024e454485a73b1f9

这里也是我导入的 get-ens 库的来源:https://github.com/talentlessguy/get-ens

【问题讨论】:

  • 到目前为止我没有发现任何问题。您是否尝试在useEffect 中放置断点以检查它是否按预期执行?
  • 唯一让我感到困惑的是 - const ac = new AbortController() 被放置在组件之外。因此,每当调用一次ac.abort() 时,其他钩子实例都无法发送请求。这应该在自定义钩子内声明 - 或者更好的是,在相关的 useEffect() 内声明
  • @skyboyer,我尝试在 useEffect 中放置一个console.log,它也只触发了一次,这是在 useEffect 中初始化中止控制器时的日志:gist.github.com/talentlessguy/82e396dbf9b7ff4da718c0f02b687964

标签: reactjs unit-testing jestjs react-hooks-testing-library


【解决方案1】:

async/await 未正确转译以执行 useEffect 清理。我不知道为什么。我自己也遇到过同样的问题。

如果您使用 Promise 样式编写,useEffect 清理将正常运行。 此外,您可能需要为每个 useEffect 使用一个新的 Abort Controller。

试试这样:

useEffect(() => {
  const ac = new AbortController()
  if (provider && domain) {
    getENS(provider)(domain, { signal: ac.signal }).then((data)=>{
      set(data)
    }
  }
  return () => ac.abort()
}, [domain, provider])

【讨论】:

  • 它仍然失败并出现同样的错误
  • 你的 mock 支持 AbortController 吗?您可能还需要一个本地取消变量。
猜你喜欢
  • 2023-03-13
  • 2019-12-21
  • 1970-01-01
  • 2021-04-25
  • 1970-01-01
  • 2021-11-02
  • 2022-08-18
  • 2020-01-20
  • 2021-12-27
相关资源
最近更新 更多