From a218bbcf576868da4b1981689e6775f78a5985ea Mon Sep 17 00:00:00 2001 From: Maciej Jastrzebski Date: Thu, 29 Dec 2022 09:36:21 +0100 Subject: [PATCH] docs: refactor react-navigation example with React Nav team feedback (#1253) --- examples/react-navigation/README.md | 15 +++++++--- .../src/screens/DetailsScreen.js | 16 +++++----- .../src/screens/DetailsScreen.test.js | 29 ++++--------------- examples/react-navigation/src/test-utils.js | 13 --------- 4 files changed, 26 insertions(+), 47 deletions(-) diff --git a/examples/react-navigation/README.md b/examples/react-navigation/README.md index 70188d2e..89c573e1 100644 --- a/examples/react-navigation/README.md +++ b/examples/react-navigation/README.md @@ -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/)**. + diff --git a/examples/react-navigation/src/screens/DetailsScreen.js b/examples/react-navigation/src/screens/DetailsScreen.js index bc6ab19c..547b40a1 100644 --- a/examples/react-navigation/src/screens/DetailsScreen.js +++ b/examples/react-navigation/src/screens/DetailsScreen.js @@ -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 ( + navigation.goBack()} /> + ); +} +export function DetailsScreenContent({ item, onGoBack }) { return ( @@ -14,16 +18,14 @@ export default function DetailsScreen({ route }) { The number you have chosen is {item.value}. - + ); } -function BackButton() { - const navigation = useNavigation(); - +function BackButton({ onPress }) { return ( - navigation.goBack()}> + Go Back ); diff --git a/examples/react-navigation/src/screens/DetailsScreen.test.js b/examples/react-navigation/src/screens/DetailsScreen.test.js index f10a5db2..80a42f33 100644 --- a/examples/react-navigation/src/screens/DetailsScreen.test.js +++ b/examples/react-navigation/src/screens/DetailsScreen.test.js @@ -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(); + render(); expect( screen.getByRole('header', { name: 'Details for Item 100' }) @@ -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); }); diff --git a/examples/react-navigation/src/test-utils.js b/examples/react-navigation/src/test-utils.js index df621b01..ea98baf1 100644 --- a/examples/react-navigation/src/test-utils.js +++ b/examples/react-navigation/src/test-utils.js @@ -10,16 +10,3 @@ import { render } from '@testing-library/react-native'; export function renderNavigator(ui) { return render({ui}); } - -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(), - }; -}