Skip to content

docs: refactor react-navigation example with React Nav team feedback #1253

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 4 commits into from
Dec 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 11 additions & 4 deletions examples/react-navigation/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,16 @@
# RNTL example app for React Navigation

This example shows how to write integration tests using React Navigation without mocking it.
This example shows how to write integration tests using React Navigation without mocking it. Presented approach has been consulted with and influenced by React Navigation team.

There are two types of tests:
1. integration tests operating on whole navigators, they should use `renderNavigator` helper to render a navigator component used in the app. It is useful when you want to test a scenario that includes multiple screens.
2. single screen tests where you would pass mock `navigation` prop, built using `buildNavigationMock()` helper, and `route` prop to the screen component using regular `render` function.
## Recommended tests

There are two types of recommeded tests:
1. Tests operating on navigator level - these use `renderNavigator` helper to render a navigator component used in the app. It is useful when you want to test a scenario that includes multiple screens.
2. Tests operating on single screen level - these use regular `render` helper but require refactoring screen components into `Screen` and `ScreenContent` components. Where `Screen` receives React Navigation props and/or uses hooks like `useNavigation` while `ScreenContent` does not have a direct relation to React Navigation API but gets props from `Screen` and calls relevant callbacks to trigger navigation.

> Note that this example applies `includeHiddenElements: false` by default, so all queries will ignore elements on the hidden screens, e.g. inactive tabs or screens present in stack navigators. This option is enabled in `jest-setup.js` file, using `defaultIncludeHiddenElements: false` option to `configure` function.

## Non-recommended tests

There also exists another popular type of screen level tests, where users mock React Navigation objects like `navigation`, `route` and/or hooks like `useNavigation`, etc. We don't recommend this way of testing. **Mocking internal parts of the libraries is effectively testing implementation details, which goes against the Testing Library's [Guiding Principles](https://testing-library.com/docs/guiding-principles/)**.

16 changes: 9 additions & 7 deletions examples/react-navigation/src/screens/DetailsScreen.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,14 @@
import * as React from 'react';
import { StyleSheet, View, Text, Pressable } from 'react-native';
import { useNavigation } from '@react-navigation/native';

export default function DetailsScreen({ route }) {
export default function DetailsScreen({ navigation, route }) {
const item = route.params;
return (
<DetailsScreenContent item={item} onGoBack={() => navigation.goBack()} />
);
}

export function DetailsScreenContent({ item, onGoBack }) {
return (
<View>
<Text accessibilityRole="header" style={styles.header}>
Expand All @@ -14,16 +18,14 @@ export default function DetailsScreen({ route }) {
The number you have chosen is {item.value}.
</Text>

<BackButton />
<BackButton onPress={onGoBack} />
</View>
);
}

function BackButton() {
const navigation = useNavigation();

function BackButton({ onPress }) {
return (
<Pressable accessibilityRole="button" onPress={() => navigation.goBack()}>
<Pressable accessibilityRole="button" onPress={onPress}>
<Text>Go Back</Text>
</Pressable>
);
Expand Down
29 changes: 6 additions & 23 deletions examples/react-navigation/src/screens/DetailsScreen.test.js
Original file line number Diff line number Diff line change
@@ -1,35 +1,18 @@
import * as React from 'react';
import { render, screen, fireEvent } from '@testing-library/react-native';
import { useNavigation } from '@react-navigation/native';
import { buildNavigationMock } from '../test-utils';
import DetailsScreen from './DetailsScreen';

jest.mock('@react-navigation/native', () => {
const originalModule = jest.requireActual('@react-navigation/native');

return {
...originalModule,
useNavigation: jest.fn(),
};
});

let navigation;

// Reset navigation before each test
beforeEach(() => {
navigation = buildNavigationMock();
useNavigation.mockImplementation(() => navigation);
});
import { DetailsScreenContent } from './DetailsScreen';

test('Details screen contains the header and content', () => {
const params = {
const item = {
id: 100,
title: 'Item 100',
value: 100,
};

const onGoBack = jest.fn();

// Passing both navigation and route to the screen as props
render(<DetailsScreen navigation={navigation} route={{ params }} />);
render(<DetailsScreenContent item={item} onGoBack={onGoBack} />);

expect(
screen.getByRole('header', { name: 'Details for Item 100' })
Expand All @@ -38,5 +21,5 @@ test('Details screen contains the header and content', () => {

// Note: Go Back button get navigation from `useNavigation` hook
fireEvent.press(screen.getByRole('button', { name: 'Go Back' }));
expect(navigation.goBack).toHaveBeenCalledTimes(1);
expect(onGoBack).toHaveBeenCalledTimes(1);
});
13 changes: 0 additions & 13 deletions examples/react-navigation/src/test-utils.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,16 +10,3 @@ import { render } from '@testing-library/react-native';
export function renderNavigator(ui) {
return render(<NavigationContainer>{ui}</NavigationContainer>);
}

export function buildNavigationMock() {
return {
navigate: jest.fn(),
reset: jest.fn(),
goBack: jest.fn(),
dispatch: jest.fn(),
isFocused: jest.fn(() => true),
setParams: jest.fn(),
setOptions: jest.fn(),
addListener: jest.fn(),
};
}