使用异步方法测试React组件

时间:2018-05-13 19:48:45

标签: javascript reactjs promise jestjs

我有一个行为如下的组件。

  1. 渲染,显示“正在加载”。
  2. 获取一些数据。
  3. 加载后,填充状态。
  4. 重新渲染,显示已加载的数据。
  5. 代码是这样的:

    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之后才会运行;文本保留为“正在加载”。

    这是测试这样的代码的理智方式吗?你会以完全不同的方式编写这段代码吗?

    我的问题已经升级了。我正在使用high order component,这意味着我不能再使用.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 个答案:

答案 0 :(得分:1)

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

我认为绊倒你实际上是React的生命周期。从本质上讲,它归结为fetch调用的位置。 React团队不鼓励您在fetch中加入“副作用”(如constructor)。以下是官方的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中已被弃用)。仅在将组件呈现到DOM中之后,才会调用componentDidMount一次。

值得注意的是,即将推出的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

答案 1 :(得分:1)

您可以使用react-testing-librarywaitForElement来避免在您的await通话中显式地fetch并简化一些事情:

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,但希望这与您最初想要的有点接近。