react-navigation v5 の useNavigation を含んだ custom hooks のテスト

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 でラップすることである。

なので、今回以下のようなラッパーを作成した。

```MockNavigation.tsx
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 があるとする

```useCustomHooks.ts
import {useNavigation} from '@react-navigation/native';

export const useCustomHooks = () => {
  const navigation = useNavigation()

  /**
   * navigation を含んだ処理
   */
}
```

以下のようにテストを書ける。
custom hooks の中で、useNavigation を使っているため、renderHook の第二引数として wrapper を提供して、上で作成した MockNavigation を使うことでテストしたい customHooks の中にも context が入る。

```useCustomHooks.test.ts
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 を作成する。

```jest.config.js
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/.*)',
  ],
};

```

```__setup__/general.js
/* 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 をする必要がある。




## 参考

https://github.com/software-mansion/react-native-gesture-handler/issues/344