Używamy cookies, żeby zwiększyć Twoje doświadczenia na stronie
CodeWorlds

Testowanie Hooków -- Diagnostyka Systemów Wewnętrznych

Custom hooki to jak wewnętrzne systemy statku kosmicznego -- silnik grawitacyjny, system podtrzymywania życia, nawigacja. Nie widzisz ich bezpośrednio, ale musisz mieć pewność, że działają poprawnie. Do testowania hooków używamy

renderHook
z React Testing Library.

renderHook -- Uruchomienie Hooka w Izolacji

renderHook
pozwala przetestować hook bez tworzenia komponentu:

1import { renderHook, act } from '@testing-library/react';
2import { useCounter } from './useCounter';
3
4test('initializes counter with default value', () => {
5  const { result } = renderHook(() => useCounter());
6
7  expect(result.current.count).toBe(0);
8});
9
10test('initializes counter with custom value', () => {
11  const { result } = renderHook(() => useCounter(10));
12
13  expect(result.current.count).toBe(10);
14});

Testowanie zmian stanu

Zmiany stanu hooka muszą być opakowane w

act()
:

1// useCounter.js
2function useCounter(initialValue = 0) {
3  const [count, setCount] = useState(initialValue);
4
5  const increment = () => setCount(c => c + 1);
6  const decrement = () => setCount(c => c - 1);
7  const reset = () => setCount(initialValue);
8
9  return { count, increment, decrement, reset };
10}
1test('increments and decrements counter', () => {
2  const { result } = renderHook(() => useCounter(5));
3
4  expect(result.current.count).toBe(5);
5
6  act(() => {
7    result.current.increment();
8  });
9  expect(result.current.count).toBe(6);
10
11  act(() => {
12    result.current.decrement();
13  });
14  expect(result.current.count).toBe(5);
15
16  act(() => {
17    result.current.reset();
18  });
19  expect(result.current.count).toBe(5);
20});

Testowanie hooków z efektami

1// useFuelGauge.js
2function useFuelGauge(shipId) {
3  const [fuel, setFuel] = useState(null);
4  const [loading, setLoading] = useState(true);
5
6  useEffect(() => {
7    fetch(`/api/ships/${shipId}/fuel`)
8      .then(res => res.json())
9      .then(data => {
10        setFuel(data.level);
11        setLoading(false);
12      });
13  }, [shipId]);
14
15  return { fuel, loading };
16}
1test('fetches fuel level for ship', async () => {
2  global.fetch = jest.fn().mockResolvedValue({
3    json: () => Promise.resolve({ level: 85 })
4  });
5
6  const { result } = renderHook(() => useFuelGauge('enterprise'));
7
8  // Początkowo loading
9  expect(result.current.loading).toBe(true);
10  expect(result.current.fuel).toBeNull();
11
12  // Po załadowaniu
13  await waitFor(() => {
14    expect(result.current.loading).toBe(false);
15  });
16  expect(result.current.fuel).toBe(85);
17});

Testowanie hooków z propami zmiennymi

Gdy hook zależy od propsów, używamy

rerender
:

1test('refetches fuel when shipId changes', async () => {
2  global.fetch = jest.fn()
3    .mockResolvedValueOnce({
4      json: () => Promise.resolve({ level: 85 })
5    })
6    .mockResolvedValueOnce({
7      json: () => Promise.resolve({ level: 42 })
8    });
9
10  const { result, rerender } = renderHook(
11    ({ shipId }) => useFuelGauge(shipId),
12    { initialProps: { shipId: 'enterprise' } }
13  );
14
15  await waitFor(() => {
16    expect(result.current.fuel).toBe(85);
17  });
18
19  // Zmiana shipId -- rerender z nowymi propsami
20  rerender({ shipId: 'discovery' });
21
22  await waitFor(() => {
23    expect(result.current.fuel).toBe(42);
24  });
25});

Testowanie hooków z kontekstem

Hooki używające kontekstu wymagają wrappera:

1// useTheme.js
2const ThemeContext = createContext();
3
4function useTheme() {
5  const context = useContext(ThemeContext);
6  if (!context) throw new Error('useTheme must be used within ThemeProvider');
7  return context;
8}
1test('returns theme from context', () => {
2  const wrapper = ({ children }) => (
3    <ThemeContext.Provider value={{ mode: 'dark', color: 'blue' }}>
4      {children}
5    </ThemeContext.Provider>
6  );
7
8  const { result } = renderHook(() => useTheme(), { wrapper });
9
10  expect(result.current.mode).toBe('dark');
11  expect(result.current.color).toBe('blue');
12});
13
14test('throws error without provider', () => {
15  expect(() => {
16    renderHook(() => useTheme());
17  }).toThrow('useTheme must be used within ThemeProvider');
18});

Testowanie hooka useReducer

1// useMissionControl.js
2function missionReducer(state, action) {
3  switch (action.type) {
4    case 'START_MISSION':
5      return { ...state, status: 'active', mission: action.payload };
6    case 'ABORT_MISSION':
7      return { ...state, status: 'aborted', mission: null };
8    case 'COMPLETE_MISSION':
9      return { ...state, status: 'completed' };
10    default:
11      return state;
12  }
13}
14
15function useMissionControl() {
16  const [state, dispatch] = useReducer(missionReducer, {
17    status: 'idle',
18    mission: null
19  });
20
21  const startMission = (name) => dispatch({ type: 'START_MISSION', payload: name });
22  const abortMission = () => dispatch({ type: 'ABORT_MISSION' });
23  const completeMission = () => dispatch({ type: 'COMPLETE_MISSION' });
24
25  return { ...state, startMission, abortMission, completeMission };
26}
1test('manages mission lifecycle', () => {
2  const { result } = renderHook(() => useMissionControl());
3
4  // Stan początkowy
5  expect(result.current.status).toBe('idle');
6  expect(result.current.mission).toBeNull();
7
8  // Start misji
9  act(() => result.current.startMission('Apollo'));
10  expect(result.current.status).toBe('active');
11  expect(result.current.mission).toBe('Apollo');
12
13  // Zakończenie misji
14  act(() => result.current.completeMission());
15  expect(result.current.status).toBe('completed');
16});
Przejdź do CodeWorlds