React hooks/Jest/Enzyme 测试 useEffect,它在 ref 上添加和删除事件侦听器

React hooks/Jest/Enzyme Test useEffect, which adds and removes event listeners on ref

我有以下组件:

export const DeviceModule = (props: Props) => {
  const [isTooltipVisible, changeTooltipVisibility] = useState(false)
  const deviceRef = useRef(null)
  useEffect(() => {
    if (deviceRef && deviceRef.current) {
      deviceRef.current.addEventListener(EVENT_TYPE.MOUSEOVER, () => changeTooltipVisibility(true))
      deviceRef.current.addEventListener(EVENT_TYPE.MOUSEOUT, () => changeTooltipVisibility(false))
    }
    return () => {
      deviceRef.current.removeEventListener(EVENT_TYPE.MOUSEOVER, () => changeTooltipVisibility(true))
      deviceRef.current.removeEventListener(EVENT_TYPE.MOUSEOUT, () => changeTooltipVisibility(false))
    }
  })

  return (
    // some jsx. When you hover on a div, it triggers one of the event listeners and changes the state. 
  )
}

我应该如何使用 Jest 和 Enzyme 对其进行测试?

2021 年更新。请勿使用酶!

原委解释得很透彻here。 简而言之:

  1. AirBNB(他们创建的)停止支持它,而是把它交给了其他人,现在只有一个人在照顾它。
  2. 2年没更新了,也就是说不支持react 17(react 18快到了)。 React 17 有 3rd 方适配器,但每个适配器都有其问题,并且面临依赖于项目的相同问题,不能保证被支持。
  3. 功能成分(如问题中的那个)很难制造,因为酶不是为它们设计的。
  4. Enzyme 使用了一些内部 React 功能,这是不鼓励的,如果 React 发生变化,可能会产生更糟糕的问题。
  5. Jest 已经升级了几个版本,使用不同的环境,这让事情变得更加复杂。

现在的行业标准是react-testing-library也是react团队推荐的,他们也停止使用酶了。

  1. 您应该使用 jest.mock()jest.requireActual() 来部分模拟 react 模块。就是说你只需要mock useRef hook,其他保持原样。

  2. 使用 ts-jest/utilsmocked 辅助函数使您的 TS 类型正确。

  3. 使用Object.defineProperty()定义deviceRef的setter和getter方法。我们将间谍添加到 setter.

    中当前元素的 addEventListener 方法
  4. 测试后使用jest.resetAllMocks()将部分模拟react模块重置为原始版本。

例如

index.tsx:

import React, { useState, useRef, useEffect } from 'react';

interface Props {}

export enum EVENT_TYPE {
  MOUSEOVER = 'MOUSEOVER',
  MOUSEOUT = 'MOUSEOUT',
}

export const DeviceModule = (props: Props) => {
  const [isTooltipVisible, changeTooltipVisibility] = useState(false);
  const deviceRef = useRef<HTMLDivElement>(null);
  useEffect(() => {
    if (deviceRef && deviceRef.current) {
      deviceRef.current.addEventListener(EVENT_TYPE.MOUSEOVER, () => changeTooltipVisibility(true));
      deviceRef.current.addEventListener(EVENT_TYPE.MOUSEOUT, () => changeTooltipVisibility(false));
    }
    return () => {
      if (deviceRef && deviceRef.current) {
        deviceRef.current.removeEventListener(EVENT_TYPE.MOUSEOVER, () => changeTooltipVisibility(true));
        deviceRef.current.removeEventListener(EVENT_TYPE.MOUSEOUT, () => changeTooltipVisibility(false));
      }
    };
  });

  return <div ref={deviceRef}>my device module</div>;
};

index.test.tsx:

import React, { useRef } from 'react';
import { mount } from 'enzyme';
import { DeviceModule, EVENT_TYPE } from './';
import { mocked } from 'ts-jest/utils';

jest.mock('react', () => {
  const originReact = jest.requireActual('react');
  return {
    ...originReact,
    useRef: jest.fn(),
  };
});

const mUseRef = mocked(useRef);

describe('66561050', () => {
  afterAll(() => {
    jest.resetAllMocks();
  });
  it('should add event listener for device ref and do cleanup work when component unmount', () => {
    const mRef = { current: {} };
    let addEventListenerSpy!: jest.SpyInstance;
    let removeEventListenerSpy!: jest.SpyInstance;
    Object.defineProperty(mRef, 'current', {
      get() {
        return this._current;
      },
      set(current) {
        if (current) {
          addEventListenerSpy = jest.spyOn(current, 'addEventListener');
          removeEventListenerSpy = jest.spyOn(current, 'removeEventListener');
        }

        this._current = current;
      },
    });
    mUseRef.mockReturnValueOnce(mRef);
    const wrapper = mount(<DeviceModule />);
    expect(addEventListenerSpy).toBeCalledWith(EVENT_TYPE.MOUSEOVER, expect.any(Function));
    expect(addEventListenerSpy).toBeCalledWith(EVENT_TYPE.MOUSEOUT, expect.any(Function));
    wrapper.unmount();
    expect(removeEventListenerSpy).toBeCalledWith(EVENT_TYPE.MOUSEOVER, expect.any(Function));
    expect(removeEventListenerSpy).toBeCalledWith(EVENT_TYPE.MOUSEOUT, expect.any(Function));
  });
});

单元测试结果:

 PASS  examples/66561050/index.test.tsx
  66561050
    ✓ should add event listener for device ref and do cleanup work when component unmount (26 ms)

-----------|---------|----------|---------|---------|-------------------
File       | % Stmts | % Branch | % Funcs | % Lines | Uncovered Line #s 
-----------|---------|----------|---------|---------|-------------------
All files  |   80.95 |       80 |      50 |     100 |                   
 index.tsx |   80.95 |       80 |      50 |     100 | 14-19             
-----------|---------|----------|---------|---------|-------------------
Test Suites: 1 passed, 1 total
Tests:       1 passed, 1 total
Snapshots:   0 total
Time:        4.744 s

源代码:https://github.com/mrdulin/jest-v26-codelab/tree/main/examples/66561050

2021 年更新。请勿使用酶!

原委解释得很透彻here。 短篇小说:

  1. AirBNB(他们创建的)停止支持它,而是把它给了别人,现在只有一个人在支持它。
  2. 已经2年没更新了,也就是说不支持react 17(顺便说一句,react 18快到了)。 React 17 有 3rd 方适配器,但每个适配器都有其问题,并且面临依赖于项目的相同问题,无法保证被支持。
  3. 功能成分(如问题中的那个)很难制造,因为酶不是为它们设计的。
  4. Enzyme 使用了一些内部 React 功能,这是不鼓励的,如果 React 发生变化,可能会产生更糟糕的问题。
  5. Jest 已经升级了几个版本,使用不同的环境,这让事情变得更加复杂。

现在的行业标准是react-testing-library也是react团队推荐的,他们也停止使用酶了。