react-native で開発する際、ページ遷移を実装するのに最も適しているライブラリは react-navigation であろう(2020/06/13時点)。
react-navigation v5 では、navigation を行うために便利な useNavigation や useRoute などの custom hooks が提供されている。
useNavigation は、 <NavigationContainer> の中で存在する Component の Context から navigation を取ってきて使用可能にするための custom hooks である。
これらの hooks はとても便利である反面、component を持たない custom hooks の中で使う場合、@testing-library/react-hooks を使って custom hooks のテストする時に wrapper に navigation の context を渡す必要がある。
useNavigation や useRoute の返り値自体を mock しても良いが、params が想定通りに渡っているかどうかを確認したい場合、返り値を mock すると不便だったりする。
解決策
context を渡す一番てっとり速い方法は context を含んでいる provider でラップすることである。
なので、今回以下のようなラッパーを作成した。
import * as React from 'react';
import {NavigationContainer} from '@react-navigation/native';
import {createStackNavigator} from '@react-navigation/stack';
import {View} from 'react-native';
const Stack = createStackNavigator();
export const MockNavigation: React.FC = ({children}) => {
  return (
    <NavigationContainer>
      <Stack.Navigator>
        <Stack.Screen
          name="MockScreen"
          component={() => <View>{children}</View>}
        />
      </Stack.Navigator>
    </NavigationContainer>
  );
};
@testing-library/react-hooks での使い方
以下の様なcustomHooks があるとする
import {useNavigation} from '@react-navigation/native';
export const useCustomHooks = () => {
  const navigation = useNavigation()
  /**
   * navigation を含んだ処理
   */
}
以下のようにテストを書ける。
custom hooks の中で、useNavigation を使っているため、renderHook の第二引数として wrapper を提供して、上で作成した MockNavigation を使うことでテストしたい customHooks の中にも context が入る。
import {renderHook} from '@testing-library/react-hooks';
import {MockProvider} from '__mocks__/MockProvider';
import {useCustomHooks} from '__hooks__/useCustomHooks'
describe('useCustomHooks', ()=> {
  it('works',()=>{
    const {result} = renderHook(useCustomHooks, {
      wrapper: {children} => <MockNavigation>{children}</MockNavigation>
    })
    const expected = { foo: 'bar' }
    expect(result.current).toMatchObject(expected)
  })
})
注意点
上記例では react-navigation の mock は行わなかったが、react-navigation は内部で react-native-gesture-handler と react-native-reanimated というNative層を触るライブラリを使用しているため、それらの mock は行わないと jest が落ちてしまう。
セットアップは以下のように行えば良い。
jest のセットアップ
各テストで必要なファイルをmockするために、general.js作成する。
今回は react-native-gesture-handler 及び react-native-reanimated の mock を作成する。
module.exports = {
  preset: 'react-native',
  setupFiles: ['<rootDir>/__setup__/general.js'],
  transformIgnorePatterns: [
    'node_modules/(?!(jest-)?react-native|react-navigation|react-navigation-redux-helpers|@react-navigation|@react-native-community/.*)',
  ],
};
/* eslint-env jest */
jest.mock('react-native-gesture-handler', () => {
  const View = require('react-native/Libraries/Components/View/View');
  return {
    Swipeable: View,
    DrawerLayout: View,
    State: {},
    ScrollView: View,
    Slider: View,
    Switch: View,
    TextInput: View,
    ToolbarAndroid: View,
    ViewPagerAndroid: View,
    DrawerLayoutAndroid: View,
    WebView: View,
    NativeViewGestureHandler: View,
    TapGestureHandler: View,
    FlingGestureHandler: View,
    ForceTouchGestureHandler: View,
    LongPressGestureHandler: View,
    PanGestureHandler: View,
    PinchGestureHandler: View,
    RotationGestureHandler: View,
    /* Buttons */
    RawButton: View,
    BaseButton: View,
    RectButton: View,
    BorderlessButton: View,
    /* Other */
    FlatList: View,
    gestureHandlerRootHOC: jest.fn(),
    Directions: {},
  };
});
jest.mock('react-native-reanimated', () =>
  require('react-native-reanimated/mock'),
);
まとめ
- useNavigation を含んだ custom hooks のテストには、Navigation のラッパーが必要になる。
- また、jest のセットアップ時に native層を触る周辺ライブラリの mock をする必要がある。
参考