Урок 11

Тестирование данных

Как тестировать работу с API и состоянием.

Инструменты

  • Jest — тест-раннер
  • React Native Testing Library — тестирование компонентов
  • MSW — моки API
  • @testing-library/jest-native — матчеры

Установка

npm install --save-dev @testing-library/react-native jest @types/jest
npm install --save-dev msw

Тест компонента

import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';
import { Posts } from './Posts';

test('отображает список постов', async () => {
    render(<Posts />);

    await waitFor(() => {
        expect(screen.getByText('Первый пост')).toBeTruthy();
    });
});

Мок API через MSW

// mocks/handlers.ts
import { http, HttpResponse } from 'msw';

export const handlers = [
    http.get('https://api.example.com/posts', () => {
        return HttpResponse.json([
            { id: 1, title: 'Первый пост' },
            { id: 2, title: 'Второй пост' },
        ]);
    }),
];
// mocks/server.ts
import { setupServer } from 'msw/node';
import { handlers } from './handlers';

export const server = setupServer(...handlers);
// setup.ts
import { server } from './mocks/server';

beforeAll(() => server.listen());
afterEach(() => server.resetHandlers());
beforeAll(() => server.close());

Обёртка с провайдерами

function renderWithProviders(ui: React.ReactElement) {
    const queryClient = new QueryClient({
        defaultOptions: { queries: { retry: false } },
    });

    return render(
        <QueryClientProvider client={queryClient}>
            <NavigationContainer>{ui}</NavigationContainer>
        </QueryClientProvider>
    );
}

Тест формы

import { render, screen, fireEvent, waitFor } from '@testing-library/react-native';

test('отправляет форму', async () => {
    const onSubmit = jest.fn();
    render(<Form onSubmit={onSubmit} />);

    fireEvent.changeText(screen.getByPlaceholderText('Email'), 'test@example.com');
    fireEvent.press(screen.getByText('Отправить'));

    await waitFor(() => {
        expect(onSubmit).toHaveBeenCalledWith({ email: 'test@example.com' });
    });
});

Тест валидации

test('показывает ошибку при пустом поле', async () => {
    render(<Form onSubmit={jest.fn()} />);

    fireEvent.press(screen.getByText('Отправить'));

    expect(await screen.findByText('Email обязателен')).toBeTruthy();
});

Мок навигации

const mockNavigate = jest.fn();

jest.mock('@react-navigation/native', () => ({
    ...jest.requireActual('@react-navigation/native'),
    useNavigation: () => ({ navigate: mockNavigate, goBack: jest.fn() }),
    useRoute: () => ({ params: { id: 5 } }),
}));

Тест хука

import { renderHook, waitFor } from '@testing-library/react-native';

test('usePosts загружает посты', async () => {
    const { result } = renderHook(() => usePosts(), {
        wrapper: QueryWrapper,
    });

    await waitFor(() => expect(result.current.data).toBeDefined());
    expect(result.current.data).toHaveLength(2);
});

Мок SecureStore

jest.mock('expo-secure-store', () => ({
    getItemAsync: jest.fn((key) => Promise.resolve(key === 'token' ? 'test-token' : null)),
    setItemAsync: jest.fn(),
    deleteItemAsync: jest.fn(),
}));

Мок NetInfo

jest.mock('@react-native-community/netinfo', () => ({
    addEventListener: jest.fn((cb) => {
        cb({ isConnected: true });
        return jest.fn();
    }),
}));

Что тестировать

  • Рендер компонентов
  • Загрузка и ошибки
  • Отправка формы
  • Валидация
  • Навигация
  • Хуки

Что не тестировать

  • Внутренние детали реализации
  • Стили
  • Сторонние библиотеки

Итоги

  • React Native Testing Library
  • MSW для моков API
  • Обёртка с провайдерами
  • Мок навигации и SecureStore
  • Тестируй поведение, не реализацию