W prawdziwym centrum kontroli lotów używa się symulatorów, aby trenować pilotów bez ryzyka uszkodzenia prawdziwego statku. W testach React mockowanie pełni dokładnie tę samą rolę -- pozwala testować komponenty bez prawdziwych połączeń z API, bazą danych czy zewnętrznymi serwisami.
jest.mock() pozwala zastąpić cały moduł jego mockiem:1// Mockujemy cały moduł API
2jest.mock('./api/missionApi');
3
4import { fetchMission, launchMission } from './api/missionApi';
5
6test('displays mission data', async () => {
7 // Ustawiamy co ma zwrócić mock
8 fetchMission.mockResolvedValue({
9 id: 1,
10 name: 'Apollo 13',
11 status: 'Active'
12 });
13
14 render(<MissionPanel />);
15
16 expect(await screen.findByText('Apollo 13')).toBeInTheDocument();
17 expect(fetchMission).toHaveBeenCalledTimes(1);
18});1jest.mock('./utils/navigation', () => ({
2 calculateRoute: jest.fn((from, to) => ({
3 distance: 100,
4 duration: '2 hours',
5 fuel: 50
6 })),
7 isValidCoordinate: jest.fn(() => true),
8}));1jest.mock('./utils/spacemath', () => ({
2 ...jest.requireActual('./utils/spacemath'),
3 // Nadpisujemy TYLKO jedną funkcję
4 calculateGravity: jest.fn(() => 9.81),
5}));1beforeEach(() => {
2 global.fetch = jest.fn();
3});
4
5afterEach(() => {
6 jest.restoreAllMocks();
7});
8
9test('fetches planets from API', async () => {
10 fetch.mockResolvedValueOnce({
11 ok: true,
12 json: () => Promise.resolve([
13 { id: 1, name: 'Mars' },
14 { id: 2, name: 'Jupiter' },
15 ]),
16 });
17
18 render(<PlanetList />);
19
20 expect(await screen.findByText('Mars')).toBeInTheDocument();
21 expect(screen.getByText('Jupiter')).toBeInTheDocument();
22 expect(fetch).toHaveBeenCalledWith('/api/planets');
23});1jest.mock('axios');
2import axios from 'axios';
3
4test('posts new mission', async () => {
5 axios.post.mockResolvedValue({
6 data: { id: 42, name: 'New Mission', status: 'Created' }
7 });
8
9 render(<CreateMissionForm />);
10
11 await user.type(screen.getByLabelText('Mission Name'), 'Artemis');
12 await user.click(screen.getByRole('button', { name: 'Create' }));
13
14 expect(axios.post).toHaveBeenCalledWith('/api/missions', {
15 name: 'Artemis'
16 });
17 expect(await screen.findByText('Mission created!')).toBeInTheDocument();
18});Mockowanie
fetch czy axios bezpośrednio działa, ale ma pewną wadę -- testujesz implementację (czy użyłeś fetch, czy axios), a nie zachowanie (czy dane zostały pobrane). MSW rozwiązuje ten problem, przechwytując requesty na poziomie sieci. To jak symulator, który udaje prawdziwą komunikację satelitarną -- Twój komponent "myśli", że rozmawia z prawdziwym serwerem, ale w rzeczywistości odpowiedzi generuje MSW. Dzięki temu test jest bardziej realistyczny i odporny na zmiany implementacji:1import { rest } from 'msw';
2import { setupServer } from 'msw/node';
3
4// Definiujemy handlery
5const handlers = [
6 rest.get('/api/missions', (req, res, ctx) => {
7 return res(
8 ctx.json([
9 { id: 1, name: 'Apollo 11', status: 'Completed' },
10 { id: 2, name: 'Artemis I', status: 'Active' },
11 ])
12 );
13 }),
14
15 rest.post('/api/missions', (req, res, ctx) => {
16 return res(
17 ctx.status(201),
18 ctx.json({ id: 3, name: req.body.name, status: 'Created' })
19 );
20 }),
21];
22
23// Tworzymy serwer
24const server = setupServer(...handlers);
25
26// Setup i teardown
27beforeAll(() => server.listen());
28afterEach(() => server.resetHandlers());
29afterAll(() => server.close());
30
31test('lists missions from API', async () => {
32 render(<MissionList />);
33
34 expect(await screen.findByText('Apollo 11')).toBeInTheDocument();
35 expect(screen.getByText('Artemis I')).toBeInTheDocument();
36});1test('handles server error', async () => {
2 // Tylko dla tego testu -- zwracamy błąd
3 server.use(
4 rest.get('/api/missions', (req, res, ctx) => {
5 return res(ctx.status(500), ctx.json({ error: 'Server error' }));
6 })
7 );
8
9 render(<MissionList />);
10
11 expect(await screen.findByText('Failed to load missions')).toBeInTheDocument();
12});Wiele komponentów korzysta z
setTimeout lub setInterval -- na przykład automatyczne odświeżanie danych co 30 sekund, odliczanie do startu misji czy animacje. W testach nie chcemy czekać prawdziwych 30 sekund! Jest udostępnia fake timery, które pozwalają "przewijać czas" jak w symulatorze lotów. Wywołujesz jest.useFakeTimers(), a potem jest.advanceTimersByTime(ms), aby przeskoczyć o określoną liczbę milisekund:1test('auto-refreshes data every 30 seconds', () => {
2 jest.useFakeTimers();
3 const mockFetch = jest.fn().mockResolvedValue({ data: [] });
4
5 render(<AutoRefreshPanel fetchData={mockFetch} interval={30000} />);
6
7 // Pierwsze wywołanie przy mount
8 expect(mockFetch).toHaveBeenCalledTimes(1);
9
10 // Po 30 sekundach -- drugie wywołanie
11 act(() => jest.advanceTimersByTime(30000));
12 expect(mockFetch).toHaveBeenCalledTimes(2);
13
14 // Po kolejnych 30 sekundach
15 act(() => jest.advanceTimersByTime(30000));
16 expect(mockFetch).toHaveBeenCalledTimes(3);
17
18 jest.useRealTimers();
19});