🧪 React Native Testing: Your App’s Safety Net
The Story of the Careful Builder
Imagine you’re building a LEGO spaceship. You wouldn’t just throw pieces together and hope it flies, right? You’d test each part:
- Does the wing click on properly?
- Do the wheels spin?
- Does the door open and close?
Testing your React Native app is exactly the same! You check each piece before launching into space (the App Store). Let’s learn how to be careful builders! 🚀
🎯 What We’ll Learn
graph TD A["Testing in React Native"] --> B["Jest Setup"] A --> C["Unit Testing"] A --> D["Mocking"] A --> E["Snapshot Testing"] A --> F["Testing Async Code"] A --> G["React Native Testing Library"] A --> H["E2E Testing Concepts"]
1️⃣ Jest Setup: Getting Your Test Kitchen Ready
The Analogy
Think of Jest as your test kitchen. Before you can bake cookies (run tests), you need:
- An oven (Jest)
- Measuring cups (configuration)
- Ingredients (your code)
What is Jest?
Jest is a testing tool made by Facebook. It comes FREE with React Native! It’s like having a robot helper that checks your work automatically.
Setting Up Jest
Good news! When you create a React Native app, Jest is already there. Let’s peek at the setup:
package.json:
{
"scripts": {
"test": "jest"
},
"jest": {
"preset": "react-native"
}
}
What does this mean?
"test": "jest"→ When you typenpm test, it runs Jest"preset": "react-native"→ Jest knows it’s testing a React Native app
Your First Test File
Create a file ending in .test.js:
// math.test.js
test('adding 1 + 1 equals 2', () => {
expect(1 + 1).toBe(2);
});
Breaking it down:
test()→ Says “Hey Jest, here’s a test!”- First part → Name of your test (what are we checking?)
expect()→ The actual check.toBe()→ Should equal this value
Running Tests
npm test
You’ll see:
✓ adding 1 + 1 equals 2
Green checkmark = SUCCESS! 🎉
2️⃣ Unit Testing: Testing One Piece at a Time
The Analogy
Imagine testing a flashlight. Unit testing means you check:
- Does the button work? (just the button)
- Does the bulb light up? (just the bulb)
- Do batteries fit? (just the battery slot)
You test each UNIT separately!
What is a Unit?
A unit is the smallest testable piece of your code:
- A function
- A component
- A calculation
Example: Testing a Function
// helpers.js
export function greet(name) {
return `Hello, ${name}!`;
}
// helpers.test.js
import { greet } from './helpers';
test('greet says hello to a name', () => {
const result = greet('Luna');
expect(result).toBe('Hello, Luna!');
});
test('greet works with any name', () => {
expect(greet('Max')).toBe('Hello, Max!');
expect(greet('Zoe')).toBe('Hello, Zoe!');
});
Testing a Simple Component
// Button.js
import React from 'react';
import { Text, TouchableOpacity } from 'react-native';
export default function Button({ label }) {
return (
<TouchableOpacity>
<Text>{label}</Text>
</TouchableOpacity>
);
}
// Button.test.js
import React from 'react';
import { render } from '@testing-library/react-native';
import Button from './Button';
test('Button shows correct label', () => {
const { getByText } = render(
<Button label="Click Me" />
);
expect(getByText('Click Me')).toBeTruthy();
});
Why Unit Testing?
| Without Testing | With Testing |
|---|---|
| 😰 “I hope this works” | 😊 “I KNOW this works” |
| 🐛 Bugs hide everywhere | 🔍 Bugs found early |
| 😱 Scared to change code | 💪 Confident changes |
3️⃣ Mocking: Pretend Helpers
The Analogy
Imagine you’re practicing a play, but your friend who plays the wizard is sick. You ask someone else to pretend to be the wizard—they say the wizard’s lines, stand in the wizard’s spot.
That’s mocking! Creating pretend versions of things.
Why Mock?
Sometimes your code needs:
- The internet (API calls)
- A database
- The phone’s camera
But in tests, we don’t want real internet or cameras. We use mocks—pretend versions!
Mocking a Function
// api.js
export async function fetchUser(id) {
// This would normally call the internet
const response = await fetch(`/users/${id}`);
return response.json();
}
// api.test.js
import { fetchUser } from './api';
// Tell Jest to use a pretend version
jest.mock('./api');
test('fetchUser returns user data', async () => {
// Set up the pretend response
fetchUser.mockResolvedValue({
id: 1,
name: 'Luna'
});
const user = await fetchUser(1);
expect(user.name).toBe('Luna');
});
Mocking React Native Modules
Some phone features need mocking:
// jest.setup.js
jest.mock('react-native', () => ({
Alert: {
alert: jest.fn()
},
Platform: {
OS: 'ios'
}
}));
Common Mock Patterns
graph TD A["What to Mock"] --> B["API Calls"] A --> C["Native Modules"] A --> D["Navigation"] A --> E["Storage"] B --> B1["fetch, axios"] C --> C1["Camera, GPS"] D --> D1["useNavigation"] E --> E1["AsyncStorage"]
Mock Example: AsyncStorage
// Mock AsyncStorage
jest.mock('@react-native-async-storage/async-storage',
() => ({
setItem: jest.fn(),
getItem: jest.fn(() => Promise.resolve('stored-value')),
removeItem: jest.fn(),
})
);
4️⃣ Snapshot Testing: Taking Photos of Your UI
The Analogy
Imagine taking a photo of your room every day. If something changes (your lamp moved), you’d notice by comparing photos!
Snapshot testing takes a “photo” of your component’s code output. If it changes unexpectedly, Jest tells you!
How It Works
// Card.js
import React from 'react';
import { View, Text } from 'react-native';
export default function Card({ title }) {
return (
<View>
<Text style={{ fontWeight: 'bold' }}>
{title}
</Text>
</View>
);
}
// Card.test.js
import React from 'react';
import renderer from 'react-test-renderer';
import Card from './Card';
test('Card matches snapshot', () => {
const tree = renderer
.create(<Card title="Hello" />)
.toJSON();
expect(tree).toMatchSnapshot();
});
What Happens?
First run: Jest creates a file called Card.test.js.snap:
exports[`Card matches snapshot 1`] = `
<View>
<Text
style={
Object {
"fontWeight": "bold",
}
}
>
Hello
</Text>
</View>
`;
Future runs: Jest compares current output to this saved snapshot.
When Snapshots Fail
If the component changes:
Snapshot failed
- Expected
+ Received
<View>
<Text
style={
Object {
- "fontWeight": "bold",
+ "fontWeight": "normal",
}
}
>
Was the change intentional?
- Yes: Run
npm test -- -uto update snapshot - No: You found a bug! Fix it!
Snapshot Best Practices
| Do ✅ | Don’t ❌ |
|---|---|
| Use for UI components | Use for logic/data |
| Keep snapshots small | Snapshot huge trees |
| Review snapshot changes | Auto-update blindly |
5️⃣ Testing Async Code: Waiting for Things
The Analogy
Imagine ordering pizza. You can’t eat it the moment you order—you have to wait! Testing async code means telling Jest to wait for things to finish.
The Problem
This test would FAIL:
// ❌ WRONG - doesn't wait!
test('fetches data', () => {
let data;
fetchData().then(result => {
data = result;
});
expect(data).toBe('hello'); // data is still undefined!
});
Solution 1: Async/Await
// ✅ CORRECT - waits properly!
test('fetches data', async () => {
const data = await fetchData();
expect(data).toBe('hello');
});
The magic words: async and await!
Solution 2: Return a Promise
test('fetches data', () => {
return fetchData().then(data => {
expect(data).toBe('hello');
});
});
Solution 3: Done Callback
test('fetches data', done => {
fetchData().then(data => {
expect(data).toBe('hello');
done(); // Tell Jest we're finished
});
});
Testing with Timers
Sometimes code uses setTimeout:
// utils.js
export function delayedHello(callback) {
setTimeout(() => {
callback('Hello!');
}, 1000);
}
// utils.test.js
jest.useFakeTimers();
test('calls callback after delay', () => {
const callback = jest.fn();
delayedHello(callback);
// Time hasn't passed yet
expect(callback).not.toHaveBeenCalled();
// Fast-forward time!
jest.runAllTimers();
expect(callback).toHaveBeenCalledWith('Hello!');
});
Async Flow
graph TD A["Start Test"] --> B{Is code async?} B -->|No| C["Run normally"] B -->|Yes| D["Use async/await"] D --> E["Jest waits..."] E --> F["Promise resolves"] F --> G["Check result"] C --> G G --> H["Test complete!"]
6️⃣ React Native Testing Library: Testing Like Users
The Analogy
Imagine testing a toy car. You could:
- Option A: Look inside at the gears and wires
- Option B: Play with it like a kid would
React Native Testing Library chooses Option B! Test what users see and do.
Why This Approach?
“The more your tests resemble the way your software is used, the more confidence they give you.”
Instead of testing internal code, test:
- What text appears?
- What happens when I tap?
- Can I find this button?
Setting Up
npm install --save-dev @testing-library/react-native
Core Methods
Finding Elements
import { render } from '@testing-library/react-native';
const { getByText, getByTestId, queryByText } =
render(<MyComponent />);
| Method | When to Use |
|---|---|
getByText |
Find by visible text |
getByTestId |
Find by testID prop |
getByRole |
Find by accessibility role |
queryByText |
Check if element exists (returns null if not) |
Testing User Actions
import { render, fireEvent } from
'@testing-library/react-native';
test('button press updates count', () => {
const { getByText } = render(<Counter />);
const button = getByText('Add');
fireEvent.press(button);
expect(getByText('Count: 1')).toBeTruthy();
});
Full Example
// LoginForm.js
import React, { useState } from 'react';
import { View, TextInput, Button, Text } from 'react-native';
export default function LoginForm({ onSubmit }) {
const [email, setEmail] = useState('');
const [error, setError] = useState('');
const handleSubmit = () => {
if (!email.includes('@')) {
setError('Invalid email');
return;
}
onSubmit(email);
};
return (
<View>
<TextInput
testID="email-input"
value={email}
onChangeText={setEmail}
placeholder="Enter email"
/>
{error ? <Text>{error}</Text> : null}
<Button title="Submit" onPress={handleSubmit} />
</View>
);
}
// LoginForm.test.js
import React from 'react';
import { render, fireEvent } from
'@testing-library/react-native';
import LoginForm from './LoginForm';
test('shows error for invalid email', () => {
const { getByTestId, getByText } =
render(<LoginForm onSubmit={() => {}} />);
const input = getByTestId('email-input');
fireEvent.changeText(input, 'notanemail');
const submitBtn = getByText('Submit');
fireEvent.press(submitBtn);
expect(getByText('Invalid email')).toBeTruthy();
});
test('calls onSubmit with valid email', () => {
const mockSubmit = jest.fn();
const { getByTestId, getByText } =
render(<LoginForm onSubmit={mockSubmit} />);
fireEvent.changeText(
getByTestId('email-input'),
'test@example.com'
);
fireEvent.press(getByText('Submit'));
expect(mockSubmit).toHaveBeenCalledWith('test@example.com');
});
7️⃣ E2E Testing Concepts: The Full Journey
The Analogy
Unit tests check individual LEGO pieces. E2E (End-to-End) tests build the entire spaceship and fly it!
E2E tests act like a real user—tapping buttons, scrolling, typing—on a real (or simulated) device.
Unit vs E2E
graph LR subgraph Unit Tests A["Test function"] --> B["Fast"] A --> C["Isolated"] end subgraph E2E Tests D["Test whole app"] --> E["Slower"] D --> F["Real device"] end
Popular E2E Tools
| Tool | Made By | Best For |
|---|---|---|
| Detox | Wix | React Native |
| Appium | Open Source | Cross-platform |
| Maestro | mobile.dev | Simple flows |
Detox Example
// e2e/login.test.js
describe('Login Flow', () => {
beforeAll(async () => {
await device.launchApp();
});
it('should login successfully', async () => {
// Type in email
await element(by.id('email-input'))
.typeText('user@test.com');
// Type password
await element(by.id('password-input'))
.typeText('secret123');
// Tap login button
await element(by.text('Login'))
.tap();
// Verify we see home screen
await expect(element(by.text('Welcome')))
.toBeVisible();
});
});
E2E Testing Pyramid
graph TD A["Testing Pyramid"] --> B["🔺 E2E Tests<br/>Few, slow, high confidence"] A --> C["🔶 Integration Tests<br/>Medium amount"] A --> D["🟩 Unit Tests<br/>Many, fast, focused"]
The idea: Have MANY unit tests, SOME integration tests, and FEW E2E tests.
When to Use E2E
✅ Good for:
- Critical user flows (login, checkout)
- Smoke tests before release
- Catching integration bugs
❌ Not good for:
- Testing every edge case
- Fast feedback during development
- Testing internal logic
E2E Best Practices
- Test critical paths only - Login, main features
- Keep tests independent - Each test starts fresh
- Use stable selectors -
testIDover text - Handle async properly - Wait for elements
- Run on CI - Automated on every push
🎯 Summary: Your Testing Toolkit
graph TD A["Your App"] --> B["Unit Tests<br/>Individual pieces"] A --> C[Snapshot Tests<br/>UI doesn't change] A --> D["Integration Tests<br/>Pieces work together"] A --> E["E2E Tests<br/>Full user journey"] B --> F["Jest + RNTL"] C --> F D --> F E --> G["Detox/Appium"]
Quick Reference
| Type | Speed | Confidence | When to Use |
|---|---|---|---|
| Unit | ⚡ Fast | 🎯 Focused | Every function |
| Snapshot | ⚡ Fast | 📸 UI structure | Components |
| Async | 🏃 Medium | ⏳ APIs/timers | Network calls |
| E2E | 🐢 Slow | 🔝 Highest | Critical flows |
You Did It! 🎉
You now understand:
- ✅ How to set up Jest
- ✅ Writing unit tests
- ✅ Creating mocks for external code
- ✅ Using snapshots for UI
- ✅ Testing async operations
- ✅ React Native Testing Library
- ✅ E2E testing concepts
Remember: Tests are your safety net! They catch bugs before users do, and give you confidence to improve your app without fear.
Happy testing! 🧪✨
