【问题标题】:how to test a hook with async state update in useEffect?如何在 useEffect 中测试带有异步状态更新的钩子?
【发布时间】:2020-12-22 12:51:51
【问题描述】:

我有一个简单的钩子来获取值并将其设置为选项,如下所示:

import Fuse from 'fuse.js'
import React from 'react'

// prefetches options and uses fuzzy search to search on that option
// instead of fetching on each keystroke
export function usePrefetchedOptions<T extends {}>(fetcher: () => Promise<T[]>) {
  const [options, setOptions] = React.useState<T[]>([])
  React.useEffect(() => {
    // fetch options initially
    const optionsFetcher = async () => {
      try {
        const data = await fetcher()
        setOptions(data)
      } catch (err) {
        errorSnack(err)
      }
    }
    optionsFetcher()
  }, [])
  // const fuseOptions = {
  //   isCaseSensitive: false,
  //   keys: ['name'],
  // }

  // const fuse = new Fuse(options, fuseOptions)

  // const dataServiceProxy = (options) => (pattern: string) => {
  //   // console.error('options inside proxy call', { options })
  //   const optionsFromSearch = fuse.search(pattern).map((fuzzyResult) => fuzzyResult.item)
  //   return new Promise((resolve) => resolve(pattern === '' ? options : optionsFromSearch))
  // }

  return options
}

我正在尝试使用以下代码对其进行测试:

import { act, renderHook, waitFor } from '@testing-library/react-hooks'
import { Wrappers } from './test-utils'
import { usePrefetchedOptions } from './usePrefetchedOptions'
import React from 'react'

const setup = ({ fetcher }) => {
  const {
    result: { current },
    waitForNextUpdate,
    ...rest
  } = renderHook(() => usePrefetchedOptions(fetcher), { wrapper: Wrappers })
  return { current, waitForNextUpdate, ...rest }
}

describe('usePrefetchedOptions', () => {
  const mockOptions = [
    {
      value: 'value1',
      text: 'Value one',
    },
    {
      value: 'value2',
      text: 'Value two',
    },
    {
      value: 'value3',
      text: 'Value three',
    },
  ]
  test('searches for appropriate option', async () => {
    const fetcher = jest.fn(() => new Promise((resolve) => resolve(mockOptions)))
    const { rerender, current: options, waitForNextUpdate } = setup({ fetcher })
    await waitFor(() => {
      expect(fetcher).toHaveBeenCalled()
    })
    // async waitForNextUpdate()
    expect(options).toHaveLength(3) // returns initial value of empty options = []
  })
})

问题是当我试图在测试结束时断言选项时,它仍然具有[] 的初始值。但是,如果我在钩子中记录值,它会返回 mockOptions。如何在通过 useEffect 但以异步方式更新钩子后更新钩子。

我也尝试过在代码中使用 waitForNextUpdate 进行注释。它超时并出现以下错误: Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Timeout - Async callback was not invoked within the 5000 ms timeout specified by jest.setTimeout.Error:

【问题讨论】:

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


    【解决方案1】:

    一些事情,目前您正在等待在您的测试中调用fetcher,但状态更新实际上不是在调用fetcher 之后发生,而是在解决fetcher 返回的承诺之后发生。因此,您需要等待测试中该承诺的解决

    另外,当你第一次渲染你的钩子时,你正在解构result.current 的值。该值只是第一次渲染后result.current 的副本,之后不会更新。要查询options 的当前值,您应该在断言中查询result.current

    const fetcherPromise = Promise.resolve(mockOptions);
    const fetch = jest.fn(() => fetcherPromise);
    const { result } = renderHook(() => usePrefetchedOptions(fetcher), { wrappers: Wrappers })
    await act(() => fetcherPromise);
    expect(result.current).toHaveLength(3) 
    

    【讨论】:

    • "另外,当你第一次渲染你的钩子时,你正在解构 result.current 的值。该值只是第一次渲染后 result.current 的副本,之后它不会更新。 "这是我在 SO 上找不到的问题的答案。谢谢。
    【解决方案2】:

    当我需要在下面测试上下文的第二个效果时,这对我有用:

    
    import React, {createContext, useContext, useEffect, useState} from "react";
    import {IGlobalContext} from "../models";
    import {fetchGravatar} from "../services";
    import {fetchTokens, Token} from "@mylib/utils";
    
    const GlobalContext = createContext<IGlobalContext>({} as IGlobalContext);
    
    
    function useGlobalProvider(): IGlobalContext {
        const [token, setToken] = useState<Token>(Token.deserialize(undefined));
        const [gravatar, setGravatar] = useState<string>('');
    
        useEffect(() => {
            setToken(fetchTokens());
        }, []);
    
        useEffect(() => {
            if (token?.getIdToken()?.getUsername()) {
                fetchGravatar(token.getIdToken().getUsername())
                    .then(setGravatar)
            }
        }, [token]);
    
        const getToken = (): Token => token;
        const getGravatar = (): string => gravatar;
    
        return {
            getToken,
            getGravatar
        }
    }
    
    const GlobalProvider: React.FC = ({children}) => {
        const globalContextData: IGlobalContext = useGlobalProvider();
        return (
            <GlobalContext.Provider value={globalContextData}>{children}</GlobalContext.Provider>
        );
    };
    
    
    function useGlobalContext() {
        if (!useContext(GlobalContext)) {
            throw new Error('GlobalContext must be used within a Provider');
        }
        return useContext<IGlobalContext>(GlobalContext);
    }
    
    export {GlobalProvider, useGlobalContext};
    
    
    

    相应的测试:

    import React from "react";
    import {GlobalProvider, useGlobalContext} from './Global';
    import {act, renderHook} from "@testing-library/react-hooks";
    import utils, {IdToken, Token} from "@mylib/utils";
    import {getRandomGravatar, getRandomToken} from 'mock/Token';
    import * as myService from './services/myService';
    import {Builder} from "builder-pattern";
    import faker from "faker";
    
    
    jest.mock('@mylib/utils', () => ({
        ...jest.requireActual('@mylib/utils')
    }));
    
    
    describe("GlobalContext", () => {
        it("should set Token when context loads", () => {
            const expectedToken = getRandomToken('mytoken');
            const spyFetchToken = spyOn(utils, 'fetchTokens').and.returnValue(expectedToken);
    
            const wrapper = ({children}: { children?: React.ReactNode }) => <GlobalProvider>{children} </GlobalProvider>;
    
            const {result} = renderHook(() => useGlobalContext(), {wrapper});
    
            expect(spyFetchToken).toHaveBeenCalled();
            expect(result.current.getToken()).toEqual(expectedToken);
        })
    
        it("should fetch Gravatar When Token username changes", async () => {
    
            const expectedToken = getRandomToken('mytoken');
            const expectedGravatar = getRandomGravatar();
            const returnedGravatarPromise = Promise.resolve(expectedGravatar);
            const spyFetchToken = spyOn(utils, 'fetchTokens').and.returnValue(expectedToken);
            const spyFetchGravatar = spyOn(myService, 'fetchGravatar').and.returnValue(returnedGravatarPromise);
    
    
            const wrapper = ({children}: { children?: React.ReactNode }) =>
                <GlobalProvider>{children} </GlobalProvider>;
    
            const {result, waitForValueToChange} = renderHook(() => useGlobalContext(), {wrapper});
    
            // see here
            // we need to wait for the promise to be resolved, even though the gravatar spy  returned it
            let resolvedGravatarPromise;
            act(() => {
                resolvedGravatarPromise = returnedGravatarPromise;
            })
    
            await waitForValueToChange(() => result.current.getGravatar());
    
            expect(spyFetchToken).toHaveBeenCalled();
            expect(result.current.getToken()).toEqual(expectedToken);
            expect(spyFetchGravatar).toHaveBeenCalledWith(expectedToken.getIdToken().getUsername());
            expect(resolvedGravatarPromise).toBeInstanceOf(Promise);
            expect(result.current.getGravatar()).toEqual(expectedGravatar);
    
        })
    })
    
    

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2021-04-14
      • 2020-05-23
      • 1970-01-01
      • 2021-11-07
      • 1970-01-01
      • 2023-02-16
      • 1970-01-01
      相关资源
      最近更新 更多