Skip to content

Commit 3c79864

Browse files
committed
Improve testing guide
1 parent 277e90e commit 3c79864

File tree

1 file changed

+171
-0
lines changed

1 file changed

+171
-0
lines changed

versioned_docs/version-7.x/testing.md

+171
Original file line numberDiff line numberDiff line change
@@ -783,3 +783,174 @@ In the above test, we:
783783
In a production app, we recommend using a library like [React Query](https://tanstack.com/query/) to handle data fetching and caching. The above example is for demonstration purposes only.
784784

785785
:::
786+
787+
### Re-usable components
788+
789+
To make it easier to test components that don't depend on the navigation structure, we can create a light-weight test navigator:
790+
791+
```js title="TestStackNavigator.js"
792+
import { useNavigationBuilder, StackRouter } from '@react-navigation/native';
793+
794+
function TestStackNavigator(props) {
795+
const { state, descriptors, NavigationContent } = useNavigationBuilder(
796+
StackRouter,
797+
props
798+
);
799+
800+
return (
801+
<NavigationContent>
802+
{state.routes.map((route, index) => {
803+
return (
804+
<View key={route.key} aria-hidden={index !== state.index}>
805+
{descriptors[route.key].render()}
806+
</View>
807+
);
808+
})}
809+
</NavigationContent>
810+
);
811+
}
812+
813+
export const createTestStackNavigator =
814+
createNavigatorFactory(TestStackNavigator);
815+
```
816+
817+
This lets us test React Navigation specific logic such as `useFocusEffect` without needing to set up a full navigator.
818+
819+
We can use this test navigator in our tests like this:
820+
821+
<Tabs groupId="example" queryString="example">
822+
<TabItem value="static" label="Static" default>
823+
824+
```js title="MyComponent.test.js"
825+
import { act, render, screen } from '@testing-library/react-native';
826+
import { createStaticNavigation } from '@react-navigation/native';
827+
import { createTestStackNavigator } from './TestStackNavigator';
828+
import { MyComponent } from './MyComponent';
829+
830+
test('does not show modal when not focused', () => {
831+
const TestStack = createTestStackNavigator({
832+
screens: {
833+
A: MyComponent,
834+
B: () => null,
835+
},
836+
});
837+
838+
const Navigation = createStaticNavigation(TestStack);
839+
840+
render(
841+
<Navigation
842+
initialState={{
843+
routes: [{ name: 'A' }, { name: 'B' }],
844+
}}
845+
/>
846+
);
847+
848+
expect(screen.queryByText('Modal')).not.toBeVisible();
849+
});
850+
851+
test('shows modal when focused', () => {
852+
const TestStack = createTestStackNavigator({
853+
screens: {
854+
A: MyComponent,
855+
B: () => null,
856+
},
857+
});
858+
859+
const Navigation = createStaticNavigation(TestStack);
860+
861+
render(
862+
<Navigation
863+
initialState={{
864+
routes: [{ name: 'B' }, { name: 'A' }],
865+
}}
866+
/>
867+
);
868+
869+
expect(screen.getByText('Modal')).toBeVisible();
870+
});
871+
```
872+
873+
</TabItem>
874+
<TabItem value="dynamic" label="Dynamic">
875+
876+
```js title="MyComponent.test.js"
877+
import { act, render, screen } from '@testing-library/react-native';
878+
import { NavigationContainer } from '@react-navigation/native';
879+
import { createTestStackNavigator } from './TestStackNavigator';
880+
import { MyComponent } from './MyComponent';
881+
882+
test('does not show modal when not focused', () => {
883+
const Stack = createTestStackNavigator();
884+
885+
const TestStack = () => (
886+
<Stack.Navigator>
887+
<Stack.Screen name="A" component={MyComponent} />
888+
<Stack.Screen name="B" component={() => null} />
889+
</Stack.Navigator>
890+
);
891+
892+
render(
893+
<NavigationContainer
894+
initialState={{
895+
routes: [{ name: 'A' }, { name: 'B' }],
896+
}}
897+
>
898+
<TestStack />
899+
</NavigationContainer>
900+
);
901+
902+
expect(screen.queryByText('Modal')).not.toBeVisible();
903+
});
904+
905+
test('shows modal when focused', () => {
906+
const Stack = createTestStackNavigator();
907+
908+
const TestStack = () => (
909+
<Stack.Navigator>
910+
<Stack.Screen name="A" component={MyComponent} />
911+
<Stack.Screen name="B" component={() => null} />
912+
</Stack.Navigator>
913+
);
914+
915+
render(
916+
<NavigationContainer
917+
initialState={{
918+
routes: [{ name: 'B' }, { name: 'A' }],
919+
}}
920+
>
921+
<TestStack />
922+
</NavigationContainer>
923+
);
924+
925+
expect(screen.getByText('Modal')).toBeVisible();
926+
});
927+
```
928+
929+
</TabItem>
930+
</Tabs>
931+
932+
Here we create a test stack navigator using the `createTestStackNavigator` function. We then render the `MyComponent` component within the test navigator and assert that the modal is shown or hidden based on the focus state.
933+
934+
The `initialState` prop is used to set the initial state of the navigator, i.e. which screens are rendered in the stack and which one is focused. See [navigation state](navigation-state.md) for more information on the structure of the state object.
935+
936+
You can also pass a [`ref`](navigation-container.md#ref) to programmatically navigate in your tests.
937+
938+
The test navigator is a simplified version of the stack navigator, but it's still a real navigator and behaves like one. This means that you can use it to test any other navigation logic.
939+
940+
See [Custom navigators](custom-navigators.md) for more information on how to write custom navigators if you want adjust the behavior of the test navigator or add more functionality.
941+
942+
## Best practices
943+
944+
Generally, we recommend avoiding mocking React Navigation. Mocking can help you isolate the component you're testing, but when testing components with navigation logic, mocking means that your tests don't test for the navigation logic.
945+
946+
- Mocking APIs such as `useFocusEffect` means you're not testing the focus logic in your component.
947+
- Mocking `navigation` prop or `useNavigation` means that the `navigation` object may not have the same shape as the real one.
948+
- Asserting `navigation.navigate` calls means you only test that the function was called, not that the call was correct based on the navigation structure.
949+
- etc.
950+
951+
Avoiding mocks means additional work when writing tests, but it also means:
952+
953+
- Refactors that don't change the logic won't break the tests, e.g. changing `navigation` prop to `useNavigation`, using a different navigation action that does the same thing, etc.
954+
- Library upgrades or refactor that actually change the behavior will correctly break the tests, surfacing actual regressions.
955+
956+
Tests should break when there's a regression, not due to a refactor. Otherwise it leads to additional work to fix the tests, making it harder to know when a regression is introduced.

0 commit comments

Comments
 (0)