Skip to content

使用React Testing Library测试异步更新的React组件

异步更新的组件

最近,在一个边项目中,我们开始使用React DnD库,因为我们想要实现一个带有卡片的多容器拖放系统。

在实现功能的大部分时间后,我们决定添加一些测试以确保一切都按预期工作。在上述项目中,我们使用React Testing Library为我们的组件编写测试。

在测试拖放功能时,我们遇到了一个非常棘手的测试。这是我们的Card组件的简化版本:

import React from 'react';
import { useDrag } from 'react-dnd';

const Card = ({
  card: {
    id,
    title
  }
}) => {
  const [style, drag] = useDrag({
    item: { id, type: 'card' },
    collect: monitor => ({
      opacity: monitor.isDragging() ? 0 : 1
    })
  });

  return (
    <li className="card" id={id} ref={drag} style={style}>
      {title}
    </li>
  );
};

这是我们最初尝试编写的测试:

import React from 'react';
import { fireEvent } from '@testing-library/react';
import Card from './components/Card';
// 这是我们编写的一个小助手,用于连接redux和react-dnd
import renderDndConnected from './test_utils/renderDndConnected';

描述('<Card/>', () => {
  let card;

  beforeEach(() => {
    const utils = renderDndConnected(
      <Card card={{ id: '1', title: 'Card' }} />
    );
    card = utils.container.querySelector('.card');
  });

  it('初始透明度为1', () => {
    expect(card.style.opacity).toEqual('1');
  });

  describe('当拖动开始时', () => {
    beforeEach(() => {
      fireEvent.dragStart(card);
    });

    it('透明度为0', () => {
      expect(card.style.opacity).toEqual('0');
    });
  });
});

## 令人恐惧的`act(...)`警告

虽然测试显然没有起作用,但控制台不断地提示将测试包装在`act()`中:

When testing, code that causes React state updates should be wrapped into act(...):

act(() => { / fire events that update state / }); / assert on the output /

This ensures that you're testing the behavior the user would see in the browser.


这条消息对于确定潜在问题并不是很有帮助。它唯一强调的是测试在完成后没有立即更新组件样式。测试完成后还有待处理的更新。简单来说,测试失败是因为`dragStart`事件没有立即更新`Card`组件的样式(即设置新的`opacity`)。

顺便提一下,`Card`组件与Redux连接,这可能与问题有关,但即使没有Redux,这种情况也很可能发生。这可能是因为`collect`需要一些时间来运行并向组件发送更新。

## 解决问题

深入挖掘后,我们发现除了`act()`之外,还有其他选项,例如`waitFor()`和`waitForDomChange()`。这些选项似乎更直观,因为它们的名称和编写方式更简单(使用`async await`或promise)。然而,`waitForDomChange()`在我们的情况下无法正常工作,而我们使用的`react-testing-library`版本(随`react-scripts`一起提供)已过时,并且没有导出`waitFor()`,这让我们花了半个小时才弄清楚。

在更新了`react-testing-library`之后,我们还没有准备好开始,因为控制台开始显示以下错误:

TypeError: MutationObserver is not a constructor


这需要一些搜索,最终我们找到了[这个问题](https://github.com/testing-library/react-testing-library/issues/662),它帮助我们找到了解决方案,即将`package.json`中的`test`脚本替换为以下内容:

```json [package.json]
{
  // ...
  "scripts": {
    "test": "react-scripts test --env=jsdom-fourteen"
    // ...
  }
}

现在终于可以编写一个有效的测试了!如上所述,我们选择使用react-testing-library中的waitFor(),这实际上是对原始测试代码的唯一更改,除了上述的依赖项升级和脚本更改。在进行必要的更改后,以下是测试的代码:

```jsx import React from 'react'; import { fireEvent, waitFor } from '@testing-library/react'; // 这是我们编写的一个小助手,用于连接redux和react-dnd import renderDndConnected from './test_utils/renderDndConnected'; import Card from './components/Card';

描述('', () => { let card;

beforeEach(() => { const utils = renderDndConnected( ); card = utils.container.querySelector('.card'); });

it('初始透明度为1', () => { expect(card.style.opacity).toEqual('1'); });

describe('当拖动开始时', () => { beforeEach(() => { fireEvent.dragStart(card); });

it('透明度为0', async() => {
  await waitFor(() => expect(card.style.opacity).toEqual('0'));
});

}); });

总结

  • 有关代码导致React状态更新未包装在act(...)中的消息可能表明组件在测试结束后更新。
  • 使用waitFor()可以通过使测试异步来解决此问题,但如果使用较旧版本的react-scripts,可能需要升级react-testing-library版本。
  • 如果看到与MutationObserver相关的错误,可能需要更改test脚本以将--env=jsdom-fourteen作为参数包含进去。