Контроль качества
Мы реализовали многое: от авторизации через JWT и работы с транзакциями — до оптимизаций производительности и темизации интерфейса. Чтобы убедиться, что наше приложение работает корректно при изменениях, мы подключим юнит- и интеграционные тесты.
Сегодня мы:
- установим всё необходимое для тестирования;
- напишем тесты для компонентов
TransactionList,AddTransactionForm,LoginPage; - проверим Redux-хранилище;
- и протестируем асинхронную загрузку транзакций через API.
Установка инструментов
Для начала установим зависимости:
npm install --save-dev jest @testing-library/react @testing-library/jest-dom @testing-library/user-event ts-jest jest-environment-jsdom
Инициализируем конфигурацию для TypeScript:
npx ts-jest config:init
Обновим package.json:
"scripts": {
"test": "jest"
}
Тестирование компонента TransactionList
Этот компонент выводит список транзакций, полученных через React Query.
Файл src/components/TransactionList.tsx
Цель теста
Проверим, что:
- компонент корректно отображает список;
- отображается индикатор загрузки;
- происходит корректный вывод при получении данных.
Тест
Файл src/components/TransactionList.test.tsx
import React from 'react';
import { render, screen, waitFor } from '@testing-library/react';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import TransactionList from './TransactionList';
import * as transactionService from '../services/transactionService';
// Мокаем fetchTransactions
jest.mock('../services/transactionService');
const mockedFetch = transactionService.fetchTransactions as jest.Mock;
const mockTransactions = [
{
id: 1,
category: 'Еда',
amount: 25,
date: '2024-01-01T12:00:00Z',
type: 'expense',
},
{
id: 2,
category: 'Зарплата',
amount: 1000,
date: '2024-01-05T12:00:00Z',
type: 'income',
},
];
const renderWithQuery = () =>
render(
<QueryClientProvider client={new QueryClient()}>
<TransactionList />
</QueryClientProvider>
);
test('отображает список транзакций', async () => {
mockedFetch.mockResolvedValueOnce(mockTransactions);
renderWithQuery();
expect(screen.getByText(/Загрузка транзакций/)).toBeInTheDocument();
await waitFor(() => {
expect(screen.getByText(/Еда/)).toBeInTheDocument();
expect(screen.getByText(/Зарплата/)).toBeInTheDocument();
});
});
Тестирование компонента AddTransactionForm
Этот компонент позволяет пользователю добавить транзакцию, и обновляет кэш через useMutation.
Файл src/components/AddTransactionForm.tsx
Цель теста
Проверим, что:
- пользователь может ввести сумму и категорию;
- при отправке вызывается мутация;
- поля очищаются после отправки.
Файл src/components/AddTransactionForm.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import { QueryClient, QueryClientProvider } from '@tanstack/react-query';
import AddTransactionForm from './AddTransactionForm';
import * as service from '../services/transactionService';
jest.mock('../services/transactionService');
const mockedCreateTransaction = service.createTransaction as jest.Mock;
const renderForm = () =>
render(
<QueryClientProvider client={new QueryClient()}>
<AddTransactionForm />
</QueryClientProvider>
);
test('добавляет транзакцию и очищает форму', async () => {
mockedCreateTransaction.mockResolvedValueOnce({ id: 3 });
renderForm();
const amountInput = screen.getByPlaceholderText('Сумма');
const categoryInput = screen.getByPlaceholderText('Категория');
const addButton = screen.getByText('Добавить');
await userEvent.type(amountInput, '50');
await userEvent.type(categoryInput, 'Кафе');
await userEvent.click(addButton);
expect(mockedCreateTransaction).toHaveBeenCalledWith(
expect.objectContaining({ amount: 50, category: 'Кафе' })
);
expect(amountInput).toHaveValue('');
expect(categoryInput).toHaveValue('');
});
Тестирование страницы LoginPage
Файл src/pages/LoginPage.tsx
Компонент отвечает за вход пользователя и сохранение токена.
Цель теста
Проверим, что:
- отображаются поля ввода;
- при успешном логине вызывается
AuthService.login; - навигация срабатывает.
Файл src/pages/LoginPage.test.tsx
import React from 'react';
import { render, screen } from '@testing-library/react';
import userEvent from '@testing-library/user-event';
import LoginPage from './LoginPage';
import * as authService from '../services/AuthService';
import { useAuth } from '../hooks/useAuth';
jest.mock('../services/AuthService');
jest.mock('../hooks/useAuth');
const mockedLogin = authService.login as jest.Mock;
const mockedUseAuth = useAuth as jest.Mock;
test('выполняет вход и вызывает login()', async () => {
mockedLogin.mockResolvedValueOnce('mock-token');
const loginMock = jest.fn();
mockedUseAuth.mockReturnValue({ login: loginMock });
render(<LoginPage />);
await userEvent.type(screen.getByPlaceholderText(/Email/), 'user@example.com');
await userEvent.type(screen.getByPlaceholderText(/Пароль/), 'password');
await userEvent.click(screen.getByRole('button', { name: /Войти/i }));
expect(mockedLogin).toHaveBeenCalledWith('user@example.com', 'password');
expect(loginMock).toHaveBeenCalledWith('mock-token');
});
Тестирование Redux-хранилища
Файлы src/store.ts, src/redux/transactionSlice.ts
Проверим, что редьюсер transactionSlice корректно обрабатывает действия.
Файл src/redux/transactionSlice.test.ts
import reducer, { addTransaction } from './transactionSlice';
test('добавляет транзакцию', () => {
const initialState = { transactions: [] };
const action = addTransaction({
id: 1,
category: 'Продукты',
amount: 120,
date: new Date().toISOString(),
type: 'expense',
});
const result = reducer(initialState, action);
expect(result.transactions).toHaveLength(1);
expect(result.transactions[0].category).toBe('Продукты');
});
Тестирование API: transactionService
Многие наши компоненты используют сервис fetchTransactions, createTransaction, и важно проверить, что они работают корректно.
Файл src/services/transactionService.test.ts
import axios from 'axios';
import { fetchTransactions } from './transactionService';
jest.mock('axios');
const mockedAxios = axios as jest.Mocked<typeof axios>;
test('получает список транзакций', async () => {
const mockData = [{ id: 1, category: 'Еда', amount: 100 }];
mockedAxios.get.mockResolvedValue({ data: mockData });
const result = await fetchTransactions();
expect(mockedAxios.get).toHaveBeenCalledWith('/transactions');
expect(result).toEqual(mockData);
});
Надеюсь, теперь тестирование кажется чуть менее пугающим! Впереди вас ждёт ещё больше интересного об инструментах и методологиях, которые сделают ваш код непотопляемым.
ПЕРЕЙДИТЕ В ПОЛНУЮ ВЕРСИЮ