skip to Main Content

I currently encountered a case that I want to write a unit test for my custom hook, said useMyHook in which called useLocation

a simple case of the hook is sth. like this:

import { useState, useEffect } from 'react'
import { useLocation } from 'react-router-dom'

const useMyHook = () => {
  const { pathname } = useLocation()
  const { myState, setMyState } = useState()
  useEffect(() => {
    switch(pathname) {
    case '/':
      setMyState('a')
      break
    case '/b':
      setMyState('b')
      break
    default:
      setMyState('c')
      break
    }
  }, [pathname])
  
  return myState
}

my test is like:

import { renderHook } from '@testing-library/react-hooks'
import useMyHook from '../useMyHook'

describe('useEventId', () => {
  it('path matches roleSelection', () => {
    jest.mock('react-router-dom', () => ({
      ...jest.requireActual('react-router-dom'), // since you still want to use the actual MemoryRouter
      useLocation: () => ({
        pathname: '/',
      }),
    }))
    const state = renderHook(() => useMyHook())
    expect(state).toBe('a')
  })
})

However, I kept getting this error:

useLocation() may be used only in the context of a component.

Can anyone point out what’s the proper way to test this kind of custom hook?

2

Answers


  1. You need to wrap your test environment inside a router context ( same as you do in a real router example ), but using the MemoryRouter component.

    So, you need to wrap your hook in a functional component that provides the necessary context.

    const wrapper = ({ children }) => <MemoryRouter>{children}</MemoryRouter>
    

    This is a full example of the code.

    import { renderHook } from '@testing-library/react-hooks';
    import { MemoryRouter } from 'react-router-dom';
    import useMyHook from '../useMyHook';
    
    
    const wrapper = ({ children }) => <MemoryRouter>{children}</MemoryRouter>;
    
    describe('useMyHook', () => {
      it('returns "a" when pathname is "/"', () => {
        jest.mock('react-router-dom', () => ({
          ...jest.requireActual('react-router-dom'),
          useLocation: () => ({
            pathname: '/',
          }),
        }));
    
        // Use the custom wrapper to provide the router context
        const { result } = renderHook(() => useMyHook(), { wrapper });
    
        expect(result.current).toBe('a');
      });
    
      it('returns "b" when pathname is "/b"', () => {
        jest.mock('react-router-dom', () => ({
          ...jest.requireActual('react-router-dom'),
          useLocation: () => ({
            pathname: '/b',
          }),
        }));
    
        const { result } = renderHook(() => useMyHook(), { wrapper });
    
        expect(result.current).toBe('b');
      });
    
      it('returns "c" for other paths', () => {
        jest.mock('react-router-dom', () => ({
          ...jest.requireActual('react-router-dom'),
          useLocation: () => ({
            pathname: '/unknown',
          }),
        }));
    
        const { result } = renderHook(() => useMyHook(), { wrapper });
    
        expect(result.current).toBe('c');
      });
    });
    
    Login or Signup to reply.
    1. Don’t mock react-router-dom module.

    2. useState() hook returns an array, not an object.

    3. You should wrap the hook with <MemoryRouter/> component

    e.g.

    useMyHook.ts:

    import { useState, useEffect } from 'react';
    import { useLocation } from 'react-router-dom';
    
    export const useMyHook = () => {
      const { pathname } = useLocation();
      const [myState, setMyState] = useState<string | undefined>();
      useEffect(() => {
        switch (pathname) {
          case '/':
            setMyState('a');
            break;
          case '/b':
            setMyState('b');
            break;
          default:
            setMyState('c');
            break;
        }
      }, [pathname]);
    
      return myState;
    };
    

    useMyHook.test.tsx:

    import React from 'react';
    import { renderHook } from '@testing-library/react';
    import { useMyHook } from './useMyHook';
    import { MemoryRouter } from 'react-router-dom';
    
    describe('useEventId', () => {
      it('path matches default', () => {
        const { result } = renderHook(() => useMyHook(), {
          wrapper: ({ children }) => <MemoryRouter initialEntries={['/c']}>{children}</MemoryRouter>,
        });
        expect(result.current).toBe('c');
      });
    
      it('path matches /', () => {
        const { result } = renderHook(() => useMyHook(), {
          wrapper: ({ children }) => <MemoryRouter initialEntries={['/']}>{children}</MemoryRouter>,
        });
        expect(result.current).toBe('a');
      });
    
      it('path matches /b', () => {
        const { result } = renderHook(() => useMyHook(), {
          wrapper: ({ children }) => <MemoryRouter initialEntries={['/b']}>{children}</MemoryRouter>,
        });
        expect(result.current).toBe('b');
      });
    });
    

    Test result:

     PASS  stackoverflow/78477167/useMyHook.test.tsx
      useEventId
        √ path matches default (12 ms)                                                                                                                                                                                                                       
        √ path matches / (2 ms)                                                                                                                                                                                                                              
        √ path matches /b (2 ms)                                                                                                                                                                                                                             
                                                                                                                                                                                                                                                             
    Test Suites: 1 passed, 1 total                                                                                                                                                                                                                           
    Tests:       3 passed, 3 total                                                                                                                                                                                                                           
    Snapshots:   0 total
    Time:        1.201 s, estimated 10 s
    Ran all test suites related to changed files.
    
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search