开发者问题收集

使用异步方法测试 React 组件

2018-05-13
1638

我有一个组件,其行为如下所示。

  1. 渲染,显示“正在加载”。
  2. 获取一些数据。
  3. 加载后,填充状态。
  4. 重新渲染,显示数据已加载。

代码如下:

import React from 'react';

class IpAddress extends React.Component {
  state = {
    ipAddress: null
  };

  constructor(props) {
    super(props);

    this.fetchData();
  }

  fetchData() {
    return fetch(`https://jsonip.com`)
      .then((response) => response.json())
      .then((json) => {
        this.setState({ ipAddress: json.ip });
      });
  }

  render() {
    if (!this.state.ipAddress) return <p class="Loading">Loading...</p>;

    return <p>Pretty fly IP address you have there.</p>
  }
}

export default IpAddress;

这很好用。但是 Jest 测试很费劲。使用 jest-fetch-mock 效果很好。

import React from 'react';
import ReactDOM from 'react-dom';
import { mount } from 'enzyme';

import IpAddress from './IpAddress';

it ('updates the text when an ip address has loaded', async () => {
  fetch.mockResponse('{ "ip": "some-ip" }');

  const address = mount(<IpAddress />);
  await address.instance().fetchData();

  address.update();

  expect(address.text()).toEqual("Pretty fly IP address you have there.");
});

有点遗憾,我必须调用 await address.instance().fetchData() ,只是为了确保更新已经发生。如果没有这个, fetch 的承诺或 setState 的异步特性(我不确定是哪个)直到我的 expect 之后才会运行;文本保留为“正在加载”。

这是测试此类代码的明智方法吗?您会完全不同地编写此代码吗?

我的问题已经升级。我正在使用 高阶组件 ,这意味着我不能再执行 .instance() 并使用其上的方法 - 我不确定如何返回到我未包装的 IpAddress。使用 IpAddress.wrappedComponent 不会像我预期的那样返回原始 IpAddress。

此操作失败并显示以下错误消息,不幸的是我看不懂。

Invariant Violation: Element type is invalid: expected a string (for built-in components) or a class/function (for composite components) but got: undefined. You likely forgot to export your component from the file it's defined in, or you might have mixed up default and named imports.

Check the render method of `WrapperComponent`.
2个回答

我必须承认我之前没有真正使用过 jest-fetch-mock,但从文档和我的小实验来看,它似乎用模拟版本替换了全局 fetch 。请注意,此示例中没有等待任何承诺: https://github.com/jefflau/jest-fetch-mock#simple-mock-and-assert 。它只是检查 fetch 是否使用正确的参数调用。因此,我认为您可以删除 async/await 并断言存在对 jsonip.com 的调用。

我认为让您绊倒的实际上是 React 生命周期。本质上,它归结为您放置 fetch 调用的位置。 React 团队不鼓励您在 constructor 中放置“副作用”(如 fetch )。这是官方 React 文档的描述: https://reactjs.org/docs/react-component.html#constructor 。不幸的是,我还没有找到关于 为什么 的良好文档。我认为这是因为 React 可能会在生命周期中的奇怪时间调用 constructor 。我认为这也是您必须在测试中手动调用 fetchData 函数的原因。

放置副作用的最佳实践是在 componentDidMount 中。以下是原因的合理解释: https://daveceddia.com/where-fetch-data-componentwillmount-vs-componentdidmount/ (尽管值得注意的是 componentWillMount 现在已在 React 16.2 中弃用)。 componentDidMount 仅在组件渲染到 DOM 后调用一次。

值得注意的是,随着 React 即将推出新版本,这一切将很快改变。这篇博文/会议视频详细介绍了以下内容: https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html

这种方法意味着它将在加载状态下最初渲染,但一旦请求得到解决,您可以通过设置状态来触发重新渲染。因为您在测试中使用了 Enzyme 的 mount ,所以这将调用所有必要的生命周期方法,包括 componentDidMount ,因此您应该看到模拟的 fetch 被调用。

至于高阶组件,我有时会使用一个技巧,虽然这可能不是最佳实践,但我认为这是一个非常有用的技巧。ES6 模块有一个 default 导出,以及任意数量的“常规”导出。我利用这一点多次导出组件。

React 惯例是在导入组件时使用 default 导出(即 import MyComponent from './my-component' )。这意味着您仍然可以从文件中导出其他内容。

我的技巧是 export default HOC 包装的组件,以便您可以像使用任何其他组件一样在源文件中使用它,但 将未包装的组件导出为“常规”组件。它看起来像:

export class MyComponent extends React.Component {
  ...
}

export default myHOCFunction()(MyComponent)

然后,您可以使用以下方式导入包装的组件:

import MyComponent from './my-component'

以及使用以下方式导入未包装的组件(即用于测试):

import { MyComponent } from './my-component'

这不是世界上最明确的模式,但在我看来它非常符合人体工程学。如果您想要明确,您可以执行以下操作:

export const WrappedMyComponent = myHOCFunction()(MyComponent)
export const UnwrappedMyComponent = MyComponent
40thieves
2018-05-14

您可以使用 react-testing-library waitForElement 来避免在 fetch 调用中显式地 await ,并稍微简化一些事情:

import React from "react";
import IpAddress from "./IpAddress";
import { render, cleanup, waitForElement } from "react-testing-library";

// So we can use `toHaveTextContent` in our expectations.
import "jest-dom/extend-expect";

describe("IpAddress", () => {
  beforeEach(() => {
    fetch.resetMocks();
  });

  afterEach(cleanup);

  it("updates the text when an IP address has loaded", async () => {
    fetch.mockResponseOnce(JSON.stringify({ ip: "1.2.3.4" }));

    const { getByTestId } = render(<IpAddress />);

    // If you add data-testid="ip" to your <p> in the component.
    const ipNode = await waitForElement(() => getByTestId("ip"));

    expect(ipNode).toHaveTextContent("Pretty fly IP address you have there.");
  });
});

这将自动等待您的元素出现,如果在某个超时时间内没有出现,则会失败。您仍然必须 await ,但希望这更接近您最初想要的。

Paul Mucur
2019-05-24