Utilizziamo i cookie per migliorare la tua esperienza sul sito
CodeWorlds

Mockowanie Modułów i API -- Symulator Lotów

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() -- Zastępowanie modułów

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});

Mockowanie z implementacją

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}));

Częściowe mockowanie

1jest.mock('./utils/spacemath', () => ({
2  ...jest.requireActual('./utils/spacemath'),
3  // Nadpisujemy TYLKO jedną funkcję
4  calculateGravity: jest.fn(() => 9.81),
5}));

Mockowanie fetch / axios

Mockowanie globalnego fetch

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});

Mockowanie axios

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});

MSW (Mock Service Worker) -- Zaawansowane Mockowanie API

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});

Nadpisywanie handlerów w pojedynczym teście

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});

Mockowanie timerów

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});
Vai a CodeWorlds