Testing with Jest and React Native Testing Library
Testing with Jest and React Native Testing Library
~17 Min. Lesezeit Zuletzt aktualisiert am August 8, 2026
Expo projects use Jest by default instead of Vitest from "React for Professionals" chapter 44 (Vitest is Vite-specific, Expo uses Metro as its bundler) – but the API is nearly identical, exactly as React Native Testing Library ("RNTL") is the direct counterpart to React Testing Library, just for native elements instead of the DOM.
Installation
npx expo install jest-expo --dev
npm install --save-dev @testing-library/react-native jest{
"scripts": {
"test": "jest"
},
"jest": {
"preset": "jest-expo"
}
}jest-expo is a preconfigured Jest preset SPECIFICALLY for Expo projects – it automatically handles the right transform for .tsx files, native module mocks (e.g. for expo-image), and other Expo-specific setup details you'd otherwise have to configure by hand.
Testing the Zustand store in isolation
Just like "React for Professionals" chapter 44, we start with the SIMPLEST case – particularly convenient here, since useCartStore itself can be called outside a React component:
import { describe, it, expect, beforeEach } from '@jest/globals';
import { useCartStore } from './cartStore';
describe('useCartStore', () => {
beforeEach(() => {
useCartStore.setState({ cart: [] }); // reset the store before every test
});
it('starts with an empty cart', () => {
expect(useCartStore.getState().cart).toEqual([]);
});
it('adds a product', () => {
useCartStore.getState().addProduct({ sku: 'a1', name: 'Test', price: 10 });
expect(useCartStore.getState().cart).toHaveLength(1);
});
it('removes a product by sku', () => {
useCartStore.getState().addProduct({ sku: 'a1', name: 'Test', price: 10 });
useCartStore.getState().removeProduct('a1');
expect(useCartStore.getState().cart).toEqual([]);
});
});useCartStore.setState(...)/.getState() – Zustand stores are ALSO fully usable OUTSIDE React components (the useCartStore(selector) hook is just ONE way to access the store) – ideal for tests that don't need to render a component. beforeEach resets the store before EVERY individual test, so tests don't affect each other (without the persist middleware in tests, see the warning below).
Achtung: In real tests, persist (the AsyncStorage middleware from chapter 2) would try to access native AsyncStorage, which doesn't exist in the Jest test environment. jest-expo AUTOMATICALLY mocks AsyncStorage with an in-memory version – tests still run despite this, WITHOUT real persistence between test runs.
Testing ProductCard with RNTL
import { describe, it, expect, jest } from '@jest/globals';
import { render, screen, fireEvent } from '@testing-library/react-native';
import ProductCard from './ProductCard';
describe('ProductCard', () => {
const defaultProps = {
name: 'Hiking Boots',
price: 89.99,
imageUrl: 'https://example.com/boots.jpg',
onPress: jest.fn(),
isFavorite: false,
onToggleFavorite: jest.fn(),
};
it('displays the name and price', () => {
render(<ProductCard {...defaultProps} />);
expect(screen.getByText('Hiking Boots')).toBeTruthy();
expect(screen.getByText('$89.99')).toBeTruthy();
});
it('calls onPress when the card is pressed', () => {
render(<ProductCard {...defaultProps} />);
fireEvent.press(screen.getByText('Hiking Boots'));
expect(defaultProps.onPress).toHaveBeenCalledTimes(1);
});
it('calls onToggleFavorite without triggering onPress', () => {
render(<ProductCard {...defaultProps} />);
fireEvent.press(screen.getByText('♡'));
expect(defaultProps.onToggleFavorite).toHaveBeenCalledTimes(1);
expect(defaultProps.onPress).not.toHaveBeenCalled();
});
});| Library | API difference |
|---|---|
| React Testing Library (web) | fireEvent.click(...), searches/compares against REAL DOM nodes (toBeInTheDocument()). |
| React Native Testing Library | fireEvent.press(...) (there's no "click" on mobile devices), works with React Native's OWN element tree, not the DOM (toBeTruthy() instead of toBeInTheDocument()). |
The third test example INDIRECTLY verifies the same thing as "React for Professionals" chapter 44: that event.stopPropagation() in handleToggleFavorite actually works – a bug in that line would additionally trigger onPress and make this test fail.
Bonus: custom mocks for native modules
Some libraries need an EXPLICIT mock that jest-expo doesn't provide automatically – e.g. react-native-reanimated:
// jest.setup.js:
import 'react-native-reanimated/jestSetup';Tipp: Rule of thumb identical to "React for Professionals" chapter 44: test VISIBLE behavior (what text appears, which callbacks get called), not internal implementation. A test that checks screen.getByText(...) instead of an internal state variable stays valid even after a refactor – exactly what we already prepared by making isFavorite a PROP instead of local state back in chapter 3.