使用 SetTimeout 测试自定义钩子,使用 Jest 测试 useEffect

Posted

技术标签:

【中文标题】使用 SetTimeout 测试自定义钩子,使用 Jest 测试 useEffect【英文标题】:Testing custom hook with SetTimeout and useEffect with Jest 【发布时间】:2021-12-26 05:56:59 【问题描述】:

我正在尝试测试一个使用 useEffect 和 setTimeout 的相对简单的自定义挂钩。但是,我的测试失败了,我不知道出了什么问题。

这里是钩子本身(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]);

和我的测试文件:

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

我所知道的是 exp 变量没有传递给 useTokenExpirationCheck 函数(console.log 在函数内部显示 0,当它被执行时)。所以基本上,我什至没有接触到 useEffect 本身......有什么想法会出错吗?

【问题讨论】:

【参考方案1】:

这是我的测试策略:

    我将使用redux-mock-store 创建一个模拟商店

模拟存储将创建一个已调度的操作数组,用作测试的操作日志。

这样我就可以通过store.getActions() 方法获取并断言调度的操作。

    我将使用模拟返回值模拟Date.now() 方法,以便测试不再依赖系统日期。不同时区和不同 CI/CD 服务器的系统日期可能不同。

    使用Props更新输入(useEffect的deps)并重新渲染钩子,以便我们可以测试案例:如果exp被更改

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' ]);
  );
);

测试结果:

 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.

【讨论】:

slideshowp2,这太棒了!我使用的是 Redux 工具包而不是 redux,所以我将类型更改为配置文件/注销,(按照工具包切片工作流程),但基本上我根据您的建议进行了更改,它终于可以工作了!我花在“测试”这件作品上的时间比花在钩子本身上的时间要多得多……谢谢阿根♥

以上是关于使用 SetTimeout 测试自定义钩子,使用 Jest 测试 useEffect的主要内容,如果未能解决你的问题,请参考以下文章

如何使用 JEST、Enzyme 在 React 中测试自定义钩子?

为啥 jest 不能为我正在测试的自定义 React 钩子提供 useTranslation 钩子?

如何使用 react-hooks-testing-library 测试自定义 async/await 钩子

我应该在这个包装 useSWR 的自定义钩子中测试啥?

如何使用 Mock Service Worker 在自定义钩子中模拟获取?

如何测试使用自定义 TypeScript React Hook 的组件?