Writing Unit Tests with Jest in React Native
Course: React Native Training
Topic: Writing Unit Tests with Jest
Course Link: React Native Training at JustAcademy
Demo Link: Register for a Course Demo
JustAcademy Website: Visit JustAcademy
1. Introduction to Unit Testing in React Native
Unit testing is a software testing technique in which individual functions, components, or small units of application logic are tested independently. It helps developers verify that their code behaves as expected before integrating it into the complete application.
In React Native development, unit testing is useful for validating JavaScript and TypeScript functions, React components, event handlers, conditional rendering, input validation, data transformations, and other application logic.
Jest is a JavaScript testing framework that provides test execution, assertions, mocking, setup and teardown methods, and code coverage reporting. It is commonly used in React Native projects to create and run automated tests.
1.1 Why Unit Testing Is Important
- Early bug detection: Identifies errors before they reach production.
- Reliable code changes: Helps developers refactor code without unintentionally breaking existing functionality.
- Faster debugging: Small, focused tests make it easier to locate problems.
- Improved code quality: Encourages modular, predictable, and maintainable code.
- Regression prevention: Detects when new changes break previously working behavior.
- Automated verification: Allows tests to run repeatedly during development and continuous integration.
1.2 What Is Jest?
Jest is an open-source JavaScript testing framework originally developed at Facebook, now Meta. It is designed to make automated testing convenient through a straightforward API and built-in testing utilities.
Jest can test ordinary JavaScript functions, asynchronous operations, and React Native components when configured with suitable testing utilities.
1.3 Key Features of Jest
| Feature |
Description |
| Test Runner |
Discovers and executes test files. |
| Assertions |
Checks whether actual results match expected results. |
| Mock Functions |
Simulates functions and tracks their calls and arguments. |
| Setup and Teardown |
Prepares and cleans up the test environment. |
| Code Coverage |
Reports which portions of the code are exercised by tests. |
| Snapshot Testing |
Compares rendered output with a previously saved snapshot. |
| Asynchronous Testing |
Tests Promises, asynchronous functions, and delayed results. |
2. Understanding Unit Tests, Integration Tests, and End-to-End Tests
Before writing tests, it is important to understand the different testing levels used in mobile application development.
| Testing Type |
Purpose |
Example |
| Unit Testing |
Tests a small unit in isolation. |
Checking whether a function adds two numbers correctly. |
| Component Testing |
Tests the behavior and rendering of a UI component. |
Checking whether a button displays its label and responds to a press. |
| Integration Testing |
Checks whether multiple units work together. |
Testing a form that validates input and calls a service. |
| End-to-End Testing |
Tests complete user workflows in an application. |
Opening the app, logging in, and submitting a form. |
Jest is especially useful for unit tests and can support component and integration tests when combined with React Native testing libraries. Dedicated tools such as Detox or Maestro may be used for end-to-end testing.
3. Setting Up Jest in a React Native Project
The setup depends on whether the project uses the React Native Community CLI or Expo. Many React Native projects already include Jest configuration or testing dependencies.
3.1 Check the Existing Project Configuration
Open the project's package.json file and check the available scripts and development dependencies.
{
"scripts": {
"test": "jest"
},
"devDependencies": {
"jest": "your-installed-version"
}
}
The example illustrates the structure only. Keep the actual package versions installed in your project rather than copying the placeholder version.
3.2 React Native Community CLI Projects
For a Community CLI project, check whether Jest and the React Native Jest preset are already configured. If they are missing, install compatible versions using your package manager and follow the configuration guidance for your React Native version.
npm install --save-dev jest @types/jest
TypeScript type definitions are useful when writing tests in TypeScript. The React Native Jest preset or equivalent configuration must also be available and compatible with the project's React Native and Jest versions.
3.3 Expo Projects
For Expo projects, use Expo's recommended testing setup and version-compatible dependencies. The following command can help install dependencies appropriate for the Expo SDK:
npx expo install jest-expo jest
npm install --save-dev @testing-library/react-native
Confirm the recommended setup for your Expo SDK before installing or changing dependencies. Existing projects may already have some or all of these packages.
3.4 Configure the Test Script
Add or update the test script in package.json according to your project's configuration.
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
These scripts allow developers to run tests once, rerun them while editing files, or generate a code coverage report.
3.5 Run the Test Suite
npm test
To run Jest in watch mode, use:
npm run test:watch
To generate a coverage report, use:
npm run test:coverage
When using a package manager other than npm, use the equivalent commands for that package manager.
4. Understanding the Structure of a Jest Test
A Jest test typically contains a description of the behavior being tested, the code being executed, and an assertion that checks the result.
4.1 The Basic Test Structure
test('adds two numbers', () => {
const result = 2 + 3;
expect(result).toBe(5);
});
Explanation:
test() defines an individual test case.
- The first argument describes the expected behavior.
- The callback contains the code that the test executes.
expect() receives the actual value.
toBe() checks whether the actual value matches the expected value using strict equality.
4.2 Using describe() to Group Tests
The describe() function groups related test cases together, making test files easier to read and maintain.
describe('Calculator', () => {
test('adds numbers', () => {
expect(2 + 3).toBe(5);
});
test('subtracts numbers', () => {
expect(8 - 3).toBe(5);
});
});
In this example, both tests belong to the Calculator group. Each test still runs independently and reports its own result.
4.3 Using it() Instead of test()
Jest supports both test() and it(). They are equivalent aliases and can be used according to the team's preferred style.
it('returns true for a valid condition', () => {
expect(5 > 2).toBe(true);
});
5. Writing Unit Tests for JavaScript Functions
Testing ordinary functions is one of the easiest ways to learn Jest. These tests generally do not require rendering a React Native component.
5.1 Create a Utility Function
Create a file named mathUtils.js.
export const add = (a, b) => a + b;
export const multiply = (a, b) => a * b;
export const isEven = (number) => number % 2 === 0;
5.2 Write Tests for the Functions
Create mathUtils.test.js.
import { add, multiply, isEven } from './mathUtils';
describe('Math utility functions', () => {
test('adds two numbers', () => {
expect(add(2, 3)).toBe(5);
});
test('multiplies two numbers', () => {
expect(multiply(4, 3)).toBe(12);
});
test('identifies an even number', () => {
expect(isEven(8)).toBe(true);
});
test('identifies an odd number', () => {
expect(isEven(7)).toBe(false);
});
});
These tests validate normal function behavior. A strong test suite should also cover important boundary values and invalid inputs where the function has defined behavior for them.
5.3 Test Boundary Conditions
Boundary tests check values at the edges of the valid input range. For example, a function that calculates a discount might need to handle a zero price, a zero discount, or the maximum permitted discount.
export const applyDiscount = (price, discount) => {
if (price < 0 || discount < 0 || discount > 100) {
throw new Error('Invalid input');
}
return price - (price * discount) / 100;
};
Tests for the function could include:
import { applyDiscount } from './discountUtils';
describe('applyDiscount', () => {
test('applies a valid discount', () => {
expect(applyDiscount(200, 10)).toBe(180);
});
test('returns the original price for zero discount', () => {
expect(applyDiscount(200, 0)).toBe(200);
});
test('rejects a negative price', () => {
expect(() => applyDiscount(-10, 10))
.toThrow('Invalid input');
});
test('rejects a discount greater than 100', () => {
expect(() => applyDiscount(200, 110))
.toThrow('Invalid input');
});
});
Notice that the function call is wrapped in a callback when using toThrow(). This allows Jest to catch and verify the exception.
6. Important Jest Matchers
Matchers are methods used with expect() to check the results of a test. Selecting the correct matcher helps make test expectations clear.
| Matcher |
Purpose |
Example |
toBe() |
Checks strict equality. |
expect(5).toBe(5) |
toEqual() |
Checks deep equality for objects and arrays. |
expect({a: 1}).toEqual({a: 1}) |
toBeTruthy() |
Checks whether a value is truthy. |
expect('hello').toBeTruthy() |
toBeFalsy() |
Checks whether a value is falsy. |
expect(0).toBeFalsy() |
toContain() |
Checks whether an array or string contains a value. |
expect([1, 2]).toContain(2) |
toHaveLength() |
Checks the length of a string or array. |
expect([1, 2]).toHaveLength(2) |
toBeNull() |
Checks whether a value is null. |
expect(null).toBeNull() |
toBeDefined() |
Checks whether a value is not undefined. |
expect('value').toBeDefined() |
toThrow() |
Checks whether a function throws an error. |
expect(fn).toThrow() |
toHaveBeenCalled() |
Checks whether a mock function was called. |
expect(fn).toHaveBeenCalled() |
toHaveBeenCalledWith() |
Checks the arguments passed to a mock function. |
expect(fn).toHaveBeenCalledWith('OK') |
6.1 Difference Between toBe() and toEqual()
toBe() is generally appropriate for primitive values such as numbers, strings, and booleans. It checks strict equality. toEqual() compares object and array contents recursively.
test('compares primitive values', () => {
expect(10).toBe(10);
});
test('compares object contents', () => {
expect({ name: 'Asha', age: 25 })
.toEqual({ name: 'Asha', age: 25 });
});
7. Testing React Native Components
React Native components combine JavaScript logic with user interface elements. Component tests can verify what is rendered and how the component responds to user interactions.
The React Native Testing Library provides tools such as render(), screen, and interaction helpers. It encourages testing components from the user's perspective instead of depending on internal implementation details.
7.1 Create a Simple Greeting Component
Create Greeting.js.
import React from 'react';
import { Text, View } from 'react-native';
export default function Greeting({ name }) {
return (
<View>
<Text>Hello, {name}!</Text>
</View>
);
}
7.2 Test the Greeting Component
Create Greeting.test.js.
import React from 'react';
import { render, screen } from '@testing-library/react-native';
import Greeting from './Greeting';
describe('Greeting component', () => {
test('displays the supplied name', () => {
render(<Greeting name="Asha" />);
expect(
screen.getByText('Hello, Asha!')
).toBeTruthy();
});
});
The test renders the component with a name prop and checks whether the expected greeting appears. This verifies an observable result of the component.
7.3 Test Conditional Rendering
Consider a component that displays a message depending on whether a user is logged in.
import React from 'react';
import { Text } from 'react-native';
export default function LoginStatus({ isLoggedIn }) {
return (
<Text>
{isLoggedIn ? 'Welcome back!' : 'Please log in'}
</Text>
);
}
Tests:
import React from 'react';
import { render, screen } from '@testing-library/react-native';
import LoginStatus from './LoginStatus';
describe('LoginStatus', () => {
test('shows welcome message when logged in', () => {
render(<LoginStatus isLoggedIn={true} />);
expect(screen.getByText('Welcome back!')).toBeTruthy();
});
test('shows login message when logged out', () => {
render(<LoginStatus isLoggedIn={false} />);
expect(screen.getByText('Please log in')).toBeTruthy();
});
});
These tests cover both branches of the conditional expression.
8. Testing Button Presses and User Interactions
React Native applications rely on touch interactions. Testing these interactions helps confirm that buttons and other controls trigger the expected behavior.
8.1 Create a Counter Component
Create Counter.js.
import React, { useState } from 'react';
import { Button, Text, View } from 'react-native';
export default function Counter() {
const [count, setCount] = useState(0);
return (
<View>
<Text>Count: {count}</Text>
<Button
title="Increase"
onPress={() => setCount(current => current + 1)}
/>
</View>
);
}
8.2 Write a Test for the Button
Create Counter.test.js.
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react-native';
import Counter from './Counter';
describe('Counter component', () => {
test('increments the count when pressed', () => {
render(<Counter />);
expect(screen.getByText('Count: 0')).toBeTruthy();
fireEvent.press(screen.getByText('Increase'));
expect(screen.getByText('Count: 1')).toBeTruthy();
});
});
The test verifies the initial count, simulates a button press, and confirms that the count changes. For more complex interactions, use the appropriate user-event APIs supported by your installed React Native Testing Library version.
8.3 What Should Be Tested in User Interactions?
- Whether the control is available to the user.
- Whether pressing the control triggers the correct behavior.
- Whether the displayed state changes correctly.
- Whether disabled controls prevent inappropriate actions.
- Whether repeated actions produce the expected result.
9. Testing Input Fields and Form Validation
Forms often contain validation rules for required fields, email addresses, passwords, and other user inputs. Unit and component tests help ensure that these rules work as expected.
9.1 Create an Email Validation Function
Create validation.js.
export function isValidEmail(email) {
return /^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email);
}
9.2 Write Validation Tests
import { isValidEmail } from './validation';
describe('isValidEmail', () => {
test('accepts a basic valid email address', () => {
expect(isValidEmail('[email protected]')).toBe(true);
});
test('rejects an email without an at sign', () => {
expect(isValidEmail('userexample.com')).toBe(false);
});
test('rejects an empty string', () => {
expect(isValidEmail('')).toBe(false);
});
test('rejects an address containing spaces', () => {
expect(isValidEmail('user [email protected]')).toBe(false);
});
});
This regular expression provides basic client-side validation, not complete verification of every email address permitted by email standards. Real applications should also validate important data on the server.
9.3 Test a Text Input Component
The following example checks whether the component displays the text entered by a user.
import React, { useState } from 'react';
import { Text, TextInput, View } from 'react-native';
export default function NameInput() {
const [name, setName] = useState('');
return (
<View>
<TextInput
accessibilityLabel="Name"
placeholder="Enter your name"
value={name}
onChangeText={setName}
/>
<Text>Entered name: {name}</Text>
</View>
);
}
Test:
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react-native';
import NameInput from './NameInput';
test('updates the displayed name when text is entered', () => {
render(<NameInput />);
fireEvent.changeText(
screen.getByLabelText('Name'),
'Asha'
);
expect(screen.getByText('Entered name: Asha')).toBeTruthy();
});
For a complete form, also test empty values, invalid inputs, valid submissions, validation messages, and submission callbacks.
10. Testing Asynchronous Functions
Many React Native applications perform asynchronous operations, including API calls, local storage access, and data processing. Jest supports testing Promises and asynchronous functions.
10.1 Test a Promise
function fetchMessage() {
return Promise.resolve('Data loaded');
}
test('returns the expected message', async () => {
await expect(fetchMessage()).resolves.toBe('Data loaded');
});
The async keyword allows the test to wait for the asynchronous assertion. Jest waits for the returned Promise to settle.
10.2 Test a Rejected Promise
function fetchFailure() {
return Promise.reject(new Error('Network error'));
}
test('handles a rejected Promise', async () => {
await expect(fetchFailure()).rejects.toThrow('Network error');
});
Testing rejected Promises helps ensure that error conditions are handled correctly.
10.3 Test an Async Function with try/catch
async function getUserName() {
const response = await Promise.resolve({ name: 'Asha' });
return response.name;
}
test('returns the user name', async () => {
const result = await getUserName();
expect(result).toBe('Asha');
});
Always return or await asynchronous work in tests. Otherwise, Jest may finish a test before the assertion executes.
11. Mocking Functions with Jest
Mocking means replacing a real function or dependency with a controlled substitute. Mocks are useful when a test should verify behavior without depending on a network service, analytics platform, or other external system.
11.1 Using jest.fn()
test('calls the callback with the expected value', () => {
const onSave = jest.fn();
onSave('Saved successfully');
expect(onSave).toHaveBeenCalled();
expect(onSave).toHaveBeenCalledTimes(1);
expect(onSave).toHaveBeenCalledWith('Saved successfully');
});
The mock function records how often it is called and which arguments it receives.
11.2 Mock Return Values
test('uses a mocked return value', () => {
const getStatus = jest.fn().mockReturnValue('success');
expect(getStatus()).toBe('success');
expect(getStatus).toHaveBeenCalledTimes(1);
});
Mock return values allow tests to control the output of a dependency without running its real implementation.
11.3 Mock Resolved Values
test('returns a mocked asynchronous result', async () => {
const getUser = jest.fn().mockResolvedValue({
id: 1,
name: 'Asha'
});
const user = await getUser();
expect(user).toEqual({
id: 1,
name: 'Asha'
});
});
11.4 Mock Rejected Values
test('simulates an API failure', async () => {
const getUser = jest.fn().mockRejectedValue(
new Error('Request failed')
);
await expect(getUser()).rejects.toThrow('Request failed');
});
11.5 When to Use Mocks
- To simulate API responses without making actual network requests.
- To verify that a callback receives the correct arguments.
- To simulate success and failure scenarios.
- To avoid depending on external services during tests.
- To control unpredictable or expensive dependencies.
Avoid mocking everything. Excessive mocking can hide integration problems and make tests pass even when the real application behavior is incorrect.
12. Testing API Service Functions
React Native applications often use APIs to retrieve profile information, products, notifications, and other data. A useful strategy is to separate API logic from UI components and test the service layer independently.
12.1 Create an API Service
Create userService.js.
export async function fetchUser(apiClient, userId) {
const response = await apiClient.get(`/users/${userId}`);
return response.data;
}
The service receives an API client as a dependency. This makes it easier to test the function without connecting to a real server.
12.2 Test the API Service with a Mock
import { fetchUser } from './userService';
describe('fetchUser', () => {
test('returns user data from the API client', async () => {
const apiClient = {
get: jest.fn().mockResolvedValue({
data: { id: 1, name: 'Asha' }
})
};
const user = await fetchUser(apiClient, 1);
expect(apiClient.get).toHaveBeenCalledWith('/users/1');
expect(user).toEqual({ id: 1, name: 'Asha' });
});
test('propagates API errors', async () => {
const apiClient = {
get: jest.fn().mockRejectedValue(
new Error('Network error')
)
};
await expect(fetchUser(apiClient, 1))
.rejects.toThrow('Network error');
});
});
This approach checks that the correct endpoint is requested and that the returned data or error is handled as expected. If the production service transforms errors, test that documented behavior instead of assuming errors must always be propagated.
13. Setup and Teardown Methods
Setup and teardown methods help prepare test data and clean up resources. They are particularly useful when multiple tests share similar initialization requirements.
| Method |
Purpose |
beforeEach() |
Runs before each test in its applicable scope. |
afterEach() |
Runs after each test in its applicable scope. |
beforeAll() |
Runs once before tests in its applicable scope. |
afterAll() |
Runs once after tests in its applicable scope. |
13.1 Example Using beforeEach()
describe('Shopping cart', () => {
let cart;
beforeEach(() => {
cart = [];
});
test('starts empty', () => {
expect(cart).toEqual([]);
});
test('adds a product', () => {
cart.push('Phone');
expect(cart).toContain('Phone');
});
});
Each test starts with a fresh array, preventing one test's modifications from affecting the next test.
13.2 Cleaning Up Mocks
afterEach(() => {
jest.clearAllMocks();
});
jest.clearAllMocks() clears recorded calls and related usage information for mock functions. It does not reset every mock implementation or restore replaced properties. Use jest.resetAllMocks() or jest.restoreAllMocks() when their different behavior is appropriate.
14. Testing React Native Components with Dependencies
Components may depend on navigation, context providers, state management libraries, storage, or third-party modules. These dependencies should be handled according to the purpose of the test.
14.1 Testing a Component with a Callback
Create a component that notifies its parent when a user presses a button.
import React from 'react';
import { Button, View } from 'react-native';
export default function SaveButton({ onSave }) {
return (
<View>
<Button title="Save" onPress={onSave} />
</View>
);
}
Test the callback:
import React from 'react';
import { fireEvent, render, screen } from '@testing-library/react-native';
import SaveButton from './SaveButton';
test('calls onSave when pressed', () => {
const onSave = jest.fn();
render(<SaveButton onSave={onSave} />);
fireEvent.press(screen.getByText('Save'));
expect(onSave).toHaveBeenCalledTimes(1);
});
14.2 Testing Context-Based Components
If a component reads data from React Context, render it inside the required provider. The test should provide a realistic context value and verify the component's visible behavior.
import React, { createContext, useContext } from 'react';
import { Text } from 'react-native';
const UserContext = createContext(null);
function UserName() {
const user = useContext(UserContext);
return <Text>{user.name}</Text>;
}
test('displays the user supplied by context', () => {
const wrapper = ({ children }) => (
<UserContext.Provider value={{ name: 'Asha' }}>
{children}
</UserContext.Provider>
);
const { getByText } = require('@testing-library/react-native')
.render(<UserName />, { wrapper });
expect(getByText('Asha')).toBeTruthy();
});
In a production test suite, import render at the top of the file instead of using a dynamic-looking inline import pattern. The following conventional form is preferable:
import React, { createContext, useContext } from 'react';
import { Text } from 'react-native';
import { render, screen } from '@testing-library/react-native';
const UserContext = createContext(null);
function UserName() {
const user = useContext(UserContext);
return <Text>{user.name}</Text>;
}
test('displays the user supplied by context', () => {
render(
<UserContext.Provider value={{ name: 'Asha' }}>
<UserName />
</UserContext.Provider>
);
expect(screen.getByText('Asha')).toBeTruthy();
});
15. Snapshot Testing in React Native
Snapshot testing stores a serialized representation of a component's rendered output and compares future output against it. It can help detect unexpected structural changes, but a snapshot passing does not prove that the component behaves correctly.
15.1 Create a Snapshot Test
import React from 'react';
import { render } from '@testing-library/react-native';
import Greeting from './Greeting';
test('matches the Greeting snapshot', () => {
const tree = render(<Greeting name="Asha" />).toJSON();
expect(tree).toMatchSnapshot();
});
When run for the first time, Jest creates a snapshot file for the test. Later runs compare the current output with that saved snapshot.
15.2 Updating Snapshots
If the output changes intentionally, review the snapshot difference before updating it. You can update snapshots using:
npm test -- -u
Do not blindly update snapshots just to make failing tests pass. Confirm that the changed output is expected and correct.
15.3 Limitations of Snapshot Testing
- Large snapshots can be difficult to review.
- Snapshots may change because of harmless implementation details.
- A passing snapshot does not verify every user interaction.
- Snapshots should complement meaningful behavior assertions rather than replace them.
16. Testing Error Handling and Edge Cases
Reliable applications should handle invalid input, missing data, network failures, and unexpected conditions. Tests should cover these scenarios wherever they are relevant to the function or component.
16.1 Testing Missing Data
export function getDisplayName(user) {
if (!user || !user.name) {
return 'Guest';
}
return user.name;
}
Tests:
import { getDisplayName } from './userUtils';
describe('getDisplayName', () => {
test('returns the name when available', () => {
expect(getDisplayName({ name: 'Asha' })).toBe('Asha');
});
test('returns Guest when the user is missing', () => {
expect(getDisplayName(null)).toBe('Guest');
});
test('returns Guest when the name is missing', () => {
expect(getDisplayName({})).toBe('Guest');
});
});
16.2 Common Edge Cases
- Empty strings and empty arrays.
- Null or undefined values.
- Numbers at minimum or maximum limits.
- Invalid or malformed input.
- Rejected Promises and network errors.
- Repeated button presses.
- Missing or incomplete API responses.
- Loading, success, and error states.
17. Code Coverage with Jest
Code coverage measures how much of the source code is exercised during tests. It can help identify untested areas but does not guarantee that the tests are meaningful or that the application is free from defects.
17.1 Generate a Coverage Report
npm test -- --coverage
Or run the configured script:
npm run test:coverage
Jest can generate a summary in the terminal and additional reports, depending on the configured coverage reporters.
17.2 Types of Code Coverage
| Coverage Type |
Meaning |
| Statement Coverage |
Measures the percentage of executable statements run by tests. |
| Branch Coverage |
Measures whether different decision outcomes have been exercised. |
| Function Coverage |
Measures which functions have been called during tests. |
| Line Coverage |
Measures the source lines executed by tests. |
17.3 Configure Coverage Thresholds
A project may define minimum coverage thresholds in its Jest configuration. The following example illustrates a possible configuration:
module.exports = {
collectCoverage: true,
collectCoverageFrom: [
'src/**/*.{js,jsx,ts,tsx}',
'!src/**/*.d.ts'
],
coverageThreshold: {
global: {
branches: 70,
functions: 80,
lines: 80,
statements: 80
}
}
};
Choose a configuration format compatible with your project's existing Jest setup. Thresholds are project-specific examples, not universal requirements. Ensure that the selected source patterns match the actual project structure.
17.4 Why High Coverage Is Not Enough
A test suite can execute every line of code without checking whether the results are correct. Meaningful tests should assert expected behavior, include important edge cases, and verify error handling.
18. Organizing Jest Test Files
A consistent test structure makes it easier to locate test cases, understand their purpose, and maintain them as the application grows.
18.1 Common Naming Conventions
mathUtils.test.js for tests associated with mathUtils.js.
Greeting.test.js for tests associated with Greeting.js.
userService.test.js for tests associated with userService.js.
__tests__ directories for grouping related test files when preferred by the project.
18.2 Example Project Structure
MyReactNativeApp/
├── src/
│ ├── components/
│ │ ├── Greeting.js
│ │ ├── Greeting.test.js
│ │ ├── Counter.js
│ │ └── Counter.test.js
│ ├── services/
│ │ ├── userService.js
│ │ └── userService.test.js
│ ├── utils/
│ │ ├── validation.js
│ │ └── validation.test.js
│ └── screens/
├── package.json
└── jest.config.js
This is an example structure. Jest can discover tests using supported naming conventions, so the exact folder arrangement can be adapted to the project.
19. Best Practices for Writing Jest Tests
19.1 Follow the Arrange, Act, Assert Pattern
Organize each test into three logical steps:
- Arrange: Prepare the input data, mock functions, and dependencies.
- Act: Execute the function or simulate the user interaction.
- Assert: Verify the expected result.
test('calculates the total price', () => {
// Arrange
const price = 100;
const quantity = 2;
// Act
const total = price * quantity;
// Assert
expect(total).toBe(200);
});
19.2 Write Clear Test Names
Prefer descriptions that explain the behavior being verified.
Less useful: test('test 1', ...)
Better: test('shows an error when the email is empty', ...)
19.3 Test Observable Behavior
For components, prefer checking visible text, accessibility labels, user interactions, and callback behavior. Avoid relying unnecessarily on internal state variables or implementation details.
19.4 Keep Tests Independent
One test should not depend on another test running first. Create fresh data for each test and clean up mocks or other shared resources when necessary.
19.5 Test Success and Failure Paths
Test normal behavior alongside relevant error scenarios. For an API service, this may include successful responses, rejected requests, and incomplete data.
19.6 Avoid Unnecessary Mocks
Mock external dependencies when isolation is useful, but keep the behavior under test realistic. Excessive mocks may make the test suite less representative of actual application behavior.
19.7 Use Accessible Queries
Where practical, use queries that reflect how users identify interface elements, such as accessible labels and visible text. Accessible components are generally easier to use and test.
19.8 Run Tests Frequently
Run tests while developing, before committing important changes, and as part of continuous integration. Frequent testing makes regressions easier to identify.
20. Common Jest Errors and Troubleshooting
| Problem |
Possible Cause |
Suggested Solution |
| Jest command not found |
Jest is not installed or the script is missing. |
Verify the project's development dependencies and test script. |
| Unexpected token or import syntax error |
Incompatible transformation or module configuration. |
Review the preset, Babel configuration, and package compatibility. |
| React Native module errors |
A native dependency requires special mocking or setup. |
Follow the library's Jest instructions and configure appropriate mocks. |
| Tests time out |
An asynchronous operation never settles or is not awaited. |
Await Promises and ensure asynchronous work completes. |
| Snapshot mismatch |
The component output changed or the snapshot is outdated. |
Review the difference before deciding whether to update the snapshot. |
| Mock call count is incorrect |
Mocks are reused between tests or callbacks run more than expected. |
Inspect the interaction flow and clear or reset mocks appropriately. |
| Unable to find an element |
The text, label, or expected UI is missing. |
Check the rendered output, component props, and query used in the test. |
| Coverage is unexpectedly low |
Some source files are excluded or not executed. |
Review coverage configuration and add meaningful tests for missing paths. |
20.1 Debugging a Failing Test
- Read the test name and full error message.
- Identify the expected result and the actual result.
- Run the failing test in isolation if needed.
- Check whether the test setup and mocks are correct.
- Confirm that asynchronous operations are awaited.
- Determine whether the problem is in the implementation, the test, or the environment.
- Fix the underlying issue and rerun the relevant suite.
21. Practical Project: Testing a Login Validation Utility
In this exercise, create a small validation utility and write unit tests for valid input, missing input, and invalid input.
21.1 Define the Validation Function
export function validateLogin(email, password) {
if (!email || !password) {
return {
valid: false,
message: 'Email and password are required'
};
}
if (!/^[^\\s@]+@[^\\s@]+\\.[^\\s@]+$/.test(email)) {
return {
valid: false,
message: 'Enter a valid email'
};
}
if (password.length < 8) {
return {
valid: false,
message: 'Password must contain at least 8 characters'
};
}
return {
valid: true,
message: 'Validation successful'
};
}
21.2 Write the Unit Tests
import { validateLogin } from './loginValidation';
describe('validateLogin', () => {
test('rejects missing credentials', () => {
expect(validateLogin('', '')).toEqual({
valid: false,
message: 'Email and password are required'
});
});
test('rejects an invalid email', () => {
expect(validateLogin('invalid-email', 'password123')).toEqual({
valid: false,
message: 'Enter a valid email'
});
});
test('rejects a short password', () => {
expect(validateLogin('[email protected]', '123')).toEqual({
valid: false,
message: 'Password must contain at least 8 characters'
});
});
test('accepts valid credentials', () => {
expect(validateLogin('[email protected]', 'password123')).toEqual({
valid: true,
message: 'Validation successful'
});
});
});
21.3 Expected Learning Outcomes
- Write a reusable utility function.
- Create a Jest test file.
- Group related test cases using
describe().
- Check object results using
toEqual().
- Cover both valid and invalid inputs.
- Run tests and interpret the results.
Important: This exercise demonstrates client-side validation only. Real authentication must use a secure authentication system or server-side verification and should never rely on client-side checks as a security boundary.
22. Practice Exercises
- Write a function that converts Celsius to Fahrenheit and test positive, negative, and zero temperatures.
- Create a function that checks whether a number is within a specified range and test boundary values.
- Build a React Native button that disables itself after being pressed and test the disabled behavior.
- Write tests for a form that displays an error when a required field is empty.
- Create an asynchronous service function and test both resolved and rejected Promises.
- Mock an API client and verify that it receives the correct endpoint and parameters.
- Create a component that displays a loading message, a success message, and an error message based on its props.
- Generate a coverage report and identify at least one meaningful missing test case.
23. Interview Questions and Answers
Q1. What is Jest?
Jest is a JavaScript testing framework used to execute automated tests and verify application behavior through assertions, mocks, and other testing utilities.
Q2. Why is Jest used in React Native?
Jest helps developers test JavaScript logic, utility functions, asynchronous operations, and React Native components. It also supports mocking and code coverage reporting.
Q3. What is the difference between test() and describe()?
test() defines an individual test case, whereas describe() groups related test cases under a common description.
Q4. What is expect()?
expect() receives the actual value that will be checked against an expected result using a matcher.
Q5. What is a Jest matcher?
A matcher is a method such as toBe(), toEqual(), or toContain() that checks whether a value meets a specified expectation.
Q6. What is the difference between toBe() and toEqual()?
toBe() checks strict equality, while toEqual() compares object and array contents recursively.
Q7. What is mocking?
Mocking replaces or simulates a function or dependency so tests can control its behavior and verify how it is used.
Q8. What is jest.fn()?
jest.fn() creates a mock function that records calls, arguments, and related usage information. Its implementation and return values can also be configured.
Q9. What is snapshot testing?
Snapshot testing compares a component's serialized output against a previously saved snapshot to identify unexpected changes.
Q10. What is code coverage?
Code coverage measures how much code is exercised by tests. It helps identify untested areas but does not independently prove correctness.
Q11. How do you test an asynchronous function?
Use async/await or return the Promise from the test. Jest supports Promise assertions such as resolves and rejects.
Q12. What are beforeEach() and afterEach()?
beforeEach() runs before each applicable test, while afterEach() runs after each applicable test. They are commonly used for setup and cleanup.
Q13. How do you test a React Native button press?
Render the component using React Native Testing Library, simulate a press with an appropriate interaction helper, and assert that the expected visible result or callback occurs.
Q14. Can Jest replace end-to-end testing?
No. Jest can test units and component behavior, but full end-to-end workflows on a device or simulator generally require a dedicated end-to-end testing tool.
Q15. What is regression testing?
Regression testing checks whether existing functionality continues to work after changes. Automated unit tests can detect many regressions quickly.
24. Quick Revision Checklist
- Understand the purpose of unit testing.
- Understand the role of Jest in React Native development.
- Set up a compatible Jest configuration.
- Write tests using
test(), describe(), and expect().
- Choose appropriate matchers for assertions.
- Test utility functions and boundary conditions.
- Render React Native components and verify visible behavior.
- Simulate button presses and text input changes.
- Test Promises and asynchronous functions correctly.
- Use mocks for external dependencies where appropriate.
- Apply setup and teardown methods when needed.
- Understand snapshot testing and its limitations.
- Generate and interpret code coverage reports.
- Investigate failures instead of blindly updating snapshots.
- Run tests regularly during development and before releasing changes.
25. Conclusion
Writing unit tests with Jest is an important skill for React Native developers. Jest provides the foundation for checking application logic, verifying expected results, mocking dependencies, testing asynchronous functions, and measuring code coverage. Combined with React Native Testing Library, it also supports tests for visible UI behavior and user interactions.
Start with small utility functions, then progress to components, forms, API services, error handling, and more complex application workflows. Write focused tests with clear expectations, cover important edge cases, and keep tests independent and maintainable. A well-designed test suite helps reduce regressions and makes React Native applications easier to develop and improve.
Explore the course: React Native Training | Register for a Demo | JustAcademy Official Website