【问题标题】:react-navigation-hooks: How to test useFocusEffectreact-navigation-hooks:如何测试 useFocusEffect
【发布时间】:2024-05-20 18:10:02
【问题描述】:

据我了解,应该这样做,以便 useFocusEffect 可以作为 useEffect 进行测试(模拟)。我将 useFocusEffect 用于 fetchData:

useFocusEffect(
  useCallback(() => {
    fetchData();
  }, [fetchData]),
);

错误信息: react-navigation 钩子需要导航上下文,但找不到。确保您没有忘记创建和渲染 react-navigation 应用程序容器。如果需要访问一个可选的导航对象,可以使用Context(NavigationContext),它可能会返回

软件包版本:

"jest": "^24.9.0",
"react-native": "0.61.2",
"react-navigation": "^4.0.10",
"react-navigation-hooks": "^1.1.0",
"@testing-library/react-native": "^4.0.14",

【问题讨论】:

标签: unit-testing react-native jestjs react-navigation


【解决方案1】:

假设您在测试中渲染您的组件,您需要将它包装在一个假的<NavigationContext> 中。这样做可以让useFocusEffect 查找确定组件是否已被您的应用导航聚焦所需的内容。

此示例使用来自react-native-testing-libraryrender。我认为它类似于其他渲染方法。

import { NavigationContext } from "@react-navigation/native"
import { render } from "react-native-testing-library"

// fake NavigationContext value data
const navContext = {
  isFocused: () => true,
  // addListener returns an unscubscribe function.
  addListener: jest.fn(() => jest.fn())
}

// MyComponent needs to be inside an NavigationContext, to allow useFocusEffect to function.
const { toJSON } = render(
  <NavigationContext.Provider value={navContext}>
    <MyComponent />
  </NavigationContext.Provider>
)

【讨论】:

    【解决方案2】:

    创建组件 FocusEffect

    import { useFocusEffect } from "@react-navigation/native";
    import { BackHandler } from "react-native";
    import React from "react";
    
    export default function FocusEffect({ onFocus, onFocusRemoved }) {
      useFocusEffect(
        React.useCallback(() => {
          onFocus();
    
          return () => onFocusRemoved();
        }, [onFocus, onFocusRemoved]),
      );
      return null;
    }
    

    用法示例:

    import React from 'react';
    import { Text, View } from 'react-native';
    import { FocusEffect } from './components';
        
    const App = () => {
    
    onFocus = () => {
       // ============>>>> onFocus <<<<==============
       fetchData();
    };
    
    onFocusRemoved = () => {
       // ============>>>> onFocusRemoved <<<<==============
    };
    
    return (
        <View>
           <FocusEffect
                onFocus={this.onFocus}
                onFocusRemoved={this.onFocusRemoved}
           />
           <Text>Hello, world!</Text>
       </View>
       )
    }
    export default App;
    

    【讨论】:

    • 测试样例在哪里?
    【解决方案3】:

    这只是@meshantz 上述答案的更完整版本。

    import { NavigationContext } from '@react-navigation/native';
    import { render } from '@testing-library/react-native';
    import React from 'react';
    
    // This would probably be imported from elsewhere...
    const ComponentUnderTest = () => {
      useFocusEffect(
        useCallback(() => {
          fetchData();
        }, [fetchData]),
      );
      
      return null;
    };
    
    const mockFetchData = jest.fn();
    jest.mock('fetchData', () => mockFetchData);
    
    describe('testing useFocusOnEffect in ComponentUnderTest', () => {
      afterAll(() => {
        jest.restoreAllMocks();
      });
    
      describe('when the view comes into focus', () => {
        it('calls fetchData', () => {
          const navContextValue = {
            isFocused: () => false,
            addListener: jest.fn(() => jest.fn()),
          };
    
          render(
            <NavigationContext.Provider value={navContextValue}>
              <ComponentUnderTest />
            </NavigationContext.Provider>,
          );
    
          expect(mockFetchData).toBeCalledTimes(0);
    
          render(
            <NavigationContext.Provider
              value={{
                ...navContextValue,
                isFocused: () => true,
              }}
            >
              <ComponentUnderTest />
            </NavigationContext.Provider>,
          );
    
          expect(mockFetchData).toBeCalledTimes(1);
        });
      });
    });
    
    

    【讨论】:

    • 我收到错误 TypeError: navigation.isFocused is not a function。 @meshantz。你能请教一下吗?
    • 更新:我很抱歉,它正在工作。我所做的是嘲笑这个jest.mock('react', () =&gt; ({ ...jest.requireActual('react') useContext: () =&gt; { user: 'sdfsf' } })); 的反应,结果证明这对预期是有害的。因此删除它,帮助。感谢@meshantz 的解决方案。
    • 这太棒了!非常感谢。你知道如何用打字稿写这个吗?我试过了,但是 navContext 缺少一些类型,比如:dispatch、navigation、reset、goBack。
    • @mohwarf 要满足 TypeScript 编译器,您必须将这些属性(您可以将它们存根)添加到 navContextValue,或者只需添加您想要在测试中使用/模拟的任何属性然后像const navContextValue = { ... } as NavigationContext 那样投射const navContextValue,这将阻止你看到类型消息。前者可能是更好的选择,但我有时会选择后者。
    【解决方案4】:

    代替useFocusEffect,使用useIsFocused 和useEffect,代码就可以正常工作了。

    In Your component:
    
    import React, { useEffect } from 'react';
    import { useIsFocused } from '@react-navigation/native';
    
    const Component = () => {
      const isFocused = useIsFocused();
    useEffect(() => {
        if (isFocused) {
          fetchData();
        }
      }, [isFocused]);
      return (<><View testID="child_test_id">{'render child nodes'}</View></>)
    }
    
    
    
    For Testing:
    
    
    import Component from '--path-to-component--';
    jest.mock('--path-to-fetchData--');
    jest.mock('@react-navigation/native', () => {
      return {
        useIsFocused: () => true
      };
    });
    
    it('should render child component when available', async () => {
      const mockedData = [];
      fetchData.mockImplementation(() => mockedData);
      let screen = null;
      await act(async () => {
        screen = renderer.create(<Component  />);
      });
      const childNode = screen.root.findByProps({ testID: 'child_test_id' });
      expect(childNode.children).toHaveLength(1);
    });

    【讨论】:

    • 这个关于单元测试的问题,但是你对工作代码的回答。
    • @VasylNahuliak 更新了我的代码 sn-p。请检查
    • 它的工作,但是更改单元测试专用的工作代码是不好的做法
    【解决方案5】:

    如果useFocusEffect() 中的代码对您的测试没有影响,您可以按如下方式模拟该钩子:

    jest.mock("@react-navigation/native", () => ({
      useFocusEffect: jest.fn(),
      // ...
    }));
    

    【讨论】:

    • 这没有意义,因为没有 mock 这段代码不会运行
    【解决方案6】:

    对于 TypeScript,它也需要满足类型要求,所以在我的例子中,它是通过使用 jest.requireActual 来完成的:

        const withProvider = (element, store = defaultStore) => {
          // fake NavigationContext value data
          const actualNav = jest.requireActual("@react-navigation/native");
          const navContext = {
            ...actualNav.navigation,
            navigate: () => {},
            dangerouslyGetState: () => {},
            setOptions: () => {},
            addListener: () => () => {},
            isFocused: () => true,
          };
          return (
            <NavigationContext.Provider value={navContext}>
              <MyComponent />
            </NavigationContext.Provider>
          );
        };
        
        it("renders correctly", () => {
          render(withProvider(() => <SportsBooksScreen {...defaultProps} />));
        });
    

    【讨论】: