Разработка Unit-тестов для React Native-приложения (Jest)
React Native-проекты часто приходят с Jest-конфигом в package.json, одним smoke-тестом renders correctly и нулевым покрытием бизнес-логики. Jest + React Native Testing Library дают всё необходимое для нормального тест-слоя — вопрос в том, что и как тестировать.
Конфигурация и стек
{
"jest": {
"preset": "react-native",
"setupFilesAfterFramework": ["@testing-library/react-native/extend-expect"],
"moduleNameMapper": {
"^@/(.*)$": "<rootDir>/src/$1"
},
"transformIgnorePatterns": [
"node_modules/(?!(react-native|@react-native|react-native-.*)/)"
]
}
}
transformIgnorePatterns — самая частая причина сломанных тестов. Нативные модули в node_modules написаны на ES modules без transpile. Паттерн должен включать все react-native-* пакеты, которые используются в проекте.
Стек:
- Jest — runner
- @testing-library/react-native — рендер компонентов, fireEvent
- @testing-library/user-event — симуляция пользователя
-
msw (Mock Service Worker) — мок API без подмены
fetch/axios
Тестирование хуков
Кастомные хуки — первое, что нужно покрыть. renderHook из @testing-library/react-native:
import { renderHook, act } from '@testing-library/react-native';
describe('useAuth', () => {
it('sets loading on login start and resolves user', async () => {
const mockLogin = jest.fn().mockResolvedValue({ id: '1', name: 'Test' });
jest.spyOn(authService, 'login').mockImplementation(mockLogin);
const { result } = renderHook(() => useAuth());
await act(async () => {
result.current.login('[email protected]', 'pass');
});
expect(result.current.isLoading).toBe(false);
expect(result.current.user?.name).toBe('Test');
});
});
act() обязателен для любого обновления state внутри хука — без него Jest выдаёт предупреждение и тест может пройти некорректно.
Zustand и Redux Toolkit
Zustand тестируется напрямую:
import { useUserStore } from '@/store/userStore';
test('setUser updates state', () => {
const { setUser } = useUserStore.getState();
setUser({ id: '1', name: 'Test' });
expect(useUserStore.getState().user?.name).toBe('Test');
});
Redux Toolkit — через configureStore с реальным reducer и мок-middleware:
const store = configureStore({ reducer: { user: userReducer } });
store.dispatch(setUser({ id: '1', name: 'Test' }));
expect(store.getState().user.current?.name).toBe('Test');
Не используйте jest.mock() на весь store — это делает тест бессмысленным.
Мокирование API через MSW
MSW перехватывает fetch/axios на уровне сети — мок реалистичен без подмены модулей:
import { setupServer } from 'msw/node';
import { rest } from 'msw';
const server = setupServer(
rest.get('https://api.example.com/user/:id', (req, res, ctx) => {
return res(ctx.json({ id: req.params.id, name: 'Test User' }));
})
);
beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
afterAll(() => server.close());
Для тестирования ошибок: server.use(rest.get(..., (_, res, ctx) => res(ctx.status(500)))).
Мокирование нативных модулей
react-native-async-storage, @react-native-firebase/app, react-native-permissions — всё это нативный код, который падает в Jest (нет JS-реализации). Каждый такой модуль нужно мокировать:
// __mocks__/@react-native-async-storage/async-storage.js
// Используем официальный mock из пакета:
jest.mock('@react-native-async-storage/async-storage',
() => require('@react-native-async-storage/async-storage/jest/async-storage-mock')
);
Firebase мокируем через @firebase/rules-unit-testing или полностью через jest.mock('@react-native-firebase/auth', () => ({...})).
Что реально стоит тестировать
- Бизнес-логика в хуках и сторах — обязательно
- Утилиты и трансформации данных — обязательно
- Навигация (параметры, условные переходы) — желательно через
@react-navigation/testing-library - Рендер UI-компонентов со snapshot — только для дизайн-системы, не для бизнес-экранов
Срок: 3–5 дней в зависимости от размера проекта и текущего состояния конфигурации Jest.







