Skip to content

Update testing section #1374

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

Closed
Closed
Changes from 4 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
250 changes: 224 additions & 26 deletions versioned_docs/version-7.x/testing.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,12 @@ If you're using `@react-navigation/stack`, you will only need to mock:
To add the mocks, create a file `jest/setup.js` (or any other file name of your choice) and paste the following code in it:

```js
// include this line for mocking react-native-gesture-handler
// Include this line for mocking react-native-gesture-handler
import 'react-native-gesture-handler/jestSetup';

// include this section and the NativeAnimatedHelper section for mocking react-native-reanimated
// Include this section for mocking react-native-reanimated
jest.mock('react-native-reanimated', () => {
const Reanimated = require('react-native-reanimated/mock');

// The mock for `call` immediately calls the callback which is incorrect
// So we override it with a no-op
Reanimated.default.call = () => {};

return Reanimated;
require('react-native-reanimated/mock');
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Seems we're missing a return statement

});

// Silence the warning: Animated: `useNativeDriver` is not supported because the native animated module is missing
Expand All @@ -54,53 +48,257 @@ Then we need to use this setup file in our jest config. You can add it under `se

Make sure that the path to the file in `setupFiles` is correct. Jest will run these files before running your tests, so it's the best place to put your global mocks.

If your configuration works correctly, you can skip this section, but in some unusual cases you will need to mock `react-native-screens` as well. To do so add the following code in `jest/setup.js` file:

```js
// Include this section form mocking react-native-screens
jest.mock('react-native-screens', () => {
// Require actual module instead of a mock
let screens = jest.requireActual('react-native-screens');

// All exports in react-native-screens are getters
// We cannot use spread for cloning as it will call the getters
// So we need to clone it with Object.create
screens = Object.create(
Object.getPrototypeOf(screens),
Object.getOwnPropertyDescriptors(screens)
);

return screens;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We are missing here example of actual mock of some component from react-native-screens. Try to mock Screen as a View for example.

});
```

If you're not using Jest, then you'll need to mock these modules according to the test framework you are using.

## Writing tests

We recommend using [React Native Testing Library](https://callstack.github.io/react-native-testing-library/) along with [`jest-native`](https://github.com/testing-library/jest-native) to write your tests.

Example:
We are going to write example tests illustrating the difference between `navigate` and `push` functions using Root Navigator defined below:

<Tabs groupId="example" queryString="example">
<TabItem value="static" label="Static" default>

```js

```

</TabItem>
<TabItem value="dynamic" label="Dynamic">

```js
import { Button, Text, View } from 'react-native';
import { createNativeStackNavigator } from '@react-navigation/native-stack';

const Profile = ({ navigation }) => {
return (
<View>
<Text>Profile</Text>
<Button
onPress={() => navigation.navigate('Settings')}
title="Navigate to Settings"
/>
<Button
onPress={() => navigation.push('Settings')}
title="Push Settings"
/>
<Button
onPress={() => setTimeout(() => navigation.navigate('Settings'), 10000)}
title="Navigate to Settings with 10000 ms delay"
/>
</View>
);
};

const Settings = () => {
return (
<View>
<Text>Settings</Text>
</View>
);
};

export const RootNavigator = () => {
const Stack = createNativeStackNavigator();
return (
<Stack.Navigator>
<Stack.Screen name="Profile" component={Profile} />
<Stack.Screen name="Settings" component={Settings} />
</Stack.Navigator>
);
};
```

</TabItem>
</Tabs>

`navigate` function test example:

<Tabs groupId="config" queryString="config">
<Tabs groupId="example" queryString="example">
<TabItem value="static" label="Static" default>

```js name='Testing with jest'
import * as React from 'react';
import { screen, render, fireEvent } from '@testing-library/react-native';
import { createStaticNavigation } from '@react-navigation/native';
```js

```

</TabItem>
<TabItem value="dynamic" label="Dynamic">

```js
import { expect, test } from '@jest/globals';
import { fireEvent, render, screen } from '@testing-library/react-native';
import {
createNavigationContainerRef,
NavigationContainer,
} from '@react-navigation/native';
import { RootNavigator } from './RootNavigator';

const Navigation = createStaticNavigation(RootNavigator);
test('navigates to settings screen twice', () => {
const navigation = createNavigationContainerRef();
render(
<NavigationContainer ref={navigation}>
<RootNavigator />
</NavigationContainer>
);

const button = screen.getByText('Navigate to Settings');
fireEvent.press(button);
fireEvent.press(button);

expect(navigation.getState().routes.map((route) => route.name)).toStrictEqual(
['Profile', 'Settings']
);
expect(screen.queryByText('Profile')).not.toBeOnTheScreen();
expect(screen.queryByText('Settings')).toBeOnTheScreen();
});
```

</TabItem>
</Tabs>

test('shows profile screen when View Profile is pressed', () => {
render(<Navigation />);
`push` function test example:

fireEvent.press(screen.getByText('View Profile'));
<Tabs groupId="example" queryString="example">
<TabItem value="static" label="Static" default>

expect(screen.getByText('My Profile')).toBeOnTheScreen();
```js

```

</TabItem>
<TabItem value="dynamic" label="Dynamic">

```js
import { expect, test } from '@jest/globals';
import { fireEvent, render, screen } from '@testing-library/react-native';
import {
createNavigationContainerRef,
NavigationContainer,
} from '@react-navigation/native';
import { RootNavigator } from './RootNavigator';

test('pushes settings screen twice', () => {
const navigation = createNavigationContainerRef();
render(
<NavigationContainer ref={navigation}>
<RootNavigator />
</NavigationContainer>
);

const button = screen.getByText('Push Settings');
fireEvent.press(button);
fireEvent.press(button);

expect(navigation.getState().routes.map((route) => route.name)).toStrictEqual(
['Profile', 'Settings', 'Settings']
);
expect(screen.queryByText('Profile')).not.toBeOnTheScreen();
expect(screen.queryByText('Settings')).toBeOnTheScreen();
});
```

</TabItem>
</Tabs>

For writing tests that include times functions you will need to use [Fake Timers](https://jestjs.io/docs/timer-mocks). They will replace times function implementation to use time from the fake clock.

Let's add another button to the Profile screen which uses `setTimeout`:

<Tabs groupId="example" queryString="example">
<TabItem value="static" label="Static" default>

```js

```

</TabItem>
<TabItem value="dynamic" label="Dynamic">

```js
const Profile = ({ navigation }) => {
return (
<View>
<Text>Profile</Text>
<Button
onPress={() => navigation.navigate('Settings')}
title="Navigate to Settings"
/>
<Button
onPress={() => navigation.push('Settings')}
title="Push Settings"
/>
// Added button
<Button
onPress={() => setTimeout(() => navigation.navigate('Settings'), 10000)}
title="Navigate to Settings with 10000 ms delay"
/>
</View>
);
};
```

</TabItem>
</Tabs>

Fake timers test example:

<Tabs groupId="example" queryString="example">
<TabItem value="static" label="Static" default>

```js

```

</TabItem>
<TabItem value="dynamic" label="Dynamic">

```js name='Testing with jest'
import * as React from 'react';
import { screen, render, fireEvent } from '@testing-library/react-native';
```js
import { expect, jest, test } from '@jest/globals';
import { act, fireEvent, render, screen } from '@testing-library/react-native';
import { NavigationContainer } from '@react-navigation/native';
import { RootNavigator } from './RootNavigator';

test('shows profile screen when View Profile is pressed', () => {
test('navigates to settings screen after 10000 ms delay', () => {
// Enable fake timers
jest.useFakeTimers();

render(
<NavigationContainer>
<RootNavigator />
</NavigationContainer>
);

fireEvent.press(screen.getByText('View Profile'));
fireEvent.press(screen.getByText('Navigate to Settings with 10000 ms delay'));

expect(screen.queryByText('Profile')).toBeOnTheScreen();
expect(screen.queryByText('Settings')).not.toBeOnTheScreen();

// jest.advanceTimersByTime causes React state updates
// So it should be wrapped into act
act(() => jest.advanceTimersByTime(10000));

expect(screen.getByText('My Profile')).toBeOnTheScreen();
expect(screen.queryByText('Profile')).not.toBeOnTheScreen();
expect(screen.queryByText('Settings')).toBeOnTheScreen();
});
```

Expand Down
Loading