littlesvensson
littlesvensson

Reputation: 49

Testing custom hook with SetTimeout and useEffect with Jest

I'm trying to test a relatively simple custom hook that uses useEffect and setTimeout. However, my test fails, and I cannot figure out what is wrong.

Here is the hook itself(useTokenExpirationCheck.ts)

import { useEffect } from 'react';
import { logout } from '../features/profile/profileSlice';
import { useAppDispatch } from '../store/hooks';

export default function useTokenExpirationCheck(exp: number): void {
  const dispatch = useAppDispatch();
  useEffect(() => {
    if (exp) {
      const timeToLogout = exp * 1000 - Date.now();
      setTimeout(() => {
        dispatch(logout());
      }, timeToLogout);
    }
    // eslint-disable-next-line react-hooks/exhaustive-deps
  }, [exp]);
}

and my testing file:

import { act, renderHook } from '@testing-library/react-hooks';
import useTokenExpirationCheck from './useTokenExpirationCheck';

jest.mock('../features/profile/profileSlice');
const logout = jest.fn();
const exp = Date.now() + 6000;

describe('Expiration token', () => {
  test('should logout user', async () => {
    jest.useFakeTimers();
    act(() => {
      renderHook(() => {
        useTokenExpirationCheck(exp);
      });
    });
    expect(logout).toHaveBeenCalledTimes(0);
    jest.advanceTimersByTime(60000);
    expect(logout).toHaveBeenCalledTimes(1);
  });
});

What I know is that the exp variable is somehow not passed to the useTokenExpirationCheck function (console.log showed me 0 inside of the function, when it was executed). So basically, I am not even getting to the useEffect itself... any ideas what could go wrong?

Upvotes: 3

Views: 3422

Answers (1)

Lin Du
Lin Du

Reputation: 102207

Here is my testing strategy:

  1. I will use redux-mock-store to create a mock store

The mock store will create an array of dispatched actions which serve as an action log for tests.

So that I can get and assert the dispatched actions by store.getActions() method.

  1. I will mock Date.now() method with a mocked return value so that the tests don't rely on system date anymore. System dates may be different for different timezone and different CI/CD servers.

  2. Use Props to update the inputs(deps of useEffect) and rerender the hook so that we can test the case: If exp is changed.

useTokenExpirationCheck.ts:

import { useEffect } from 'react';
import { useDispatch } from 'react-redux';

const logout = () => ({ type: 'LOGOUT' });

export default function useTokenExpirationCheck(exp: number): void {
  const dispatch = useDispatch();
  useEffect(() => {
    if (exp) {
      console.log('exp: ', exp);
      const timeToLogout = exp * 1000 - Date.now();
      setTimeout(() => {
        dispatch(logout());
      }, timeToLogout);
    }
  }, [exp]);
}

useTokenExpirationCheck.test.tsx:

import { renderHook } from '@testing-library/react-hooks';
import React from 'react';
import { Provider } from 'react-redux';
import createMockStore from 'redux-mock-store';
import useTokenExpirationCheck from './useTokenExpirationCheck';

describe('useTokenExpirationCheck', () => {
  test('should dispatch logout action after delay', async () => {
    let exp = 6000;
    jest.spyOn(Date, 'now').mockReturnValue(5900 * 1000);
    const mockStore = createMockStore([]);
    const store = mockStore({});
    jest.useFakeTimers();
    const { rerender } = renderHook(() => useTokenExpirationCheck(exp), {
      wrapper: ({ children }) => <Provider store={store}>{children}</Provider>,
    });

    jest.advanceTimersByTime(100 * 1000);
    expect(store.getActions()).toEqual([{ type: 'LOGOUT' }]);

    exp = 6100;
    rerender();
    jest.advanceTimersByTime(200 * 1000);
    expect(store.getActions()).toEqual([{ type: 'LOGOUT' }, { type: 'LOGOUT' }]);
  });
});

test result:

 PASS  examples/69967414/useTokenExpirationCheck.test.tsx (8.329 s)
  useTokenExpirationCheck
    ✓ should pass (39 ms)

  console.log
    exp:  6000

      at examples/69967414/useTokenExpirationCheck.ts:10:15

  console.log
    exp:  6100

      at examples/69967414/useTokenExpirationCheck.ts:10:15

------------------------|---------|----------|---------|---------|-------------------
File                    | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
------------------------|---------|----------|---------|---------|-------------------
All files               |     100 |       50 |     100 |     100 |                   
 ...nExpirationCheck.ts |     100 |       50 |     100 |     100 | 9                 
------------------------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        8.885 s, estimated 10 s
Ran all test suites related to changed files.

Upvotes: 2

Related Questions