简体   繁体   English

使用异步方法测试React组件

[英]Testing React Components with asynchronous methods

I have a component which behaves like the following. 我有一个行为类似于以下的组件。

  1. Render, showing the 'Loading'. 渲染,显示“正在加载”。
  2. Fetch some data. 获取一些数据。
  3. Once that's loaded, populate the state. 加载后,填充状态。
  4. Rerender, showing that the data loaded. 渲染,显示已加载数据。

The code is like this: 代码是这样的:

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;

This works fine. 这很好。 The Jest test is a struggle though. 开玩笑的测试是一个挣扎。 Using jest-fetch-mock works well. 使用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.");
});

It's a bit sad that I have to call await address.instance().fetchData() , just to make sure that the update has happened. 我不得不调用await address.instance().fetchData()有点可悲,只是为了确保更新已经完成。 Without this, the promise from fetch or the async nature of setState (I'm not sure which) don't run until after my expect ; 没有这个,来自fetch的承诺或setState的异步特性(我不确定是哪个)要等到我的expect之后才能运行; the text is left as "Loading". 文本保留为“正在加载”。

Is this the sane way to test code like this? 这是测试代码的明智方法吗? Would you write this code completely differently? 您会以完全不同的方式编写此代码吗?

My problem has since escalated. 此后,我的问题升级了。 I'm using a high order component , which means I can no longer do .instance() and use the methods on it - I'm not sure how to get back to my unwrapped IpAddress. 我使用的是高阶组件 ,这意味着我无法再执行.instance()并使用其上的方法-我不确定如何返回到未包装的IpAddress。 Using IpAddress.wrappedComponent doesn't give me back the original IpAddress, like I expected. 像我期望的那样,使用IpAddress.wrappedComponent不会把原来的IpAddress还给我。

This fails with the following error message, which I unfortunately don't understand. 失败并显示以下错误消息,很遗憾,我无法理解。

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`.

I must admit haven't used really jest-fetch-mock before, but from the docs and my little experiments, it looks like it replaces the global fetch with a mocked version. 我必须承认之前从未真正使用过jest-fetch-mock,但是从文档和我的小实验来看,它看起来像用模拟版本代替了全局fetch Notice how in this example isn't awaiting any promises: https://github.com/jefflau/jest-fetch-mock#simple-mock-and-assert . 请注意,在此示例中,如何不等待任何承诺: https : //github.com/jefflau/jest-fetch-mock#simple-mock-and-assert It's merely checking that fetch was called with the right arguments. 只是检查是否使用正确的参数调用了fetch So therefore I think you can remove the async/await and assert there is a call to jsonip.com. 因此,因此我认为您可以删除async / await并断言存在对jsonip.com的调用。

What I think is tripping you up is actually the React lifecycle. 我认为让您绊倒的实际上是React生命周期。 Essentially it boils down to where you put the fetch call. 从本质上讲,它归结为您放置fetch调用的位置。 The React team discourages you from putting "side-effects" (like fetch ) in the constructor . React团队不鼓励您将“副作用”(例如fetch )放入constructor Here's the official React docs description: https://reactjs.org/docs/react-component.html#constructor . 这是官方的React docs描述: https : //reactjs.org/docs/react-component.html#constructor Unfortunately I've not really found good documentation on why . 不幸的是,我还没有真正找到为什么的好文档。 I believe it's because React may call the constructor at odd times during the lifecycle. 我相信这是因为React在生命周期中可能会在奇怪的时间调用constructor I think it is also the reason that you're having to manually call the fetchData function in your test. 我认为这也是您必须在测试中手动调用fetchData函数的原因。

The best practice for putting side-effects is in componentDidMount . 放置副作用的最佳实践是componentDidMount Here's an ok explaination of why: https://daveceddia.com/where-fetch-data-componentwillmount-vs-componentdidmount/ (although it's worth noting that componentWillMount is now deprecated in React 16.2). 这是为什么的好解释: https : //daveceddia.com/where-fetch-data-componentwillmount-vs-componentdidmount/ (尽管值得注意的是,React 16.2现在不推荐使用componentWillMount )。 componentDidMount is called exactly once, only after the component is rendered into the DOM. componentDidMount被调用一次,组件被渲染到DOM之后。

It's also worth noting that this is all going to change soon with upcoming versions of React. 还值得注意的是,随着即将推出的React版本,这一切都将很快改变。 This blog post/conference video goes into a lot more details: https://reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html 该博客文章/会议视频包含更多详细信息: https : //reactjs.org/blog/2018/03/01/sneak-peek-beyond-react-16.html

This approach means it will render initially in the loading state, but once the request has resolved you can trigger a re-render by setting state. 这种方法意味着它将首先在加载状态下渲染,但是一旦请求解决,您可以通过设置状态来触发重新渲染。 Because you're using mount from Enzyme in your test, this will call all of the necessary lifecycle methods, including componentDidMount and so you should see the mocked fetch being called. 因为您在测试中使用的是来自Enzyme的mount ,所以它将调用所有必需的生命周期方法,包括componentDidMount ,因此您应该看到正在调用模拟fetch

As for the higher order component, there's a trick that I sometimes use that is maybe not the best practice, but I think is a pretty useful hack. 至于高阶组件,我有时会使用一个技巧,这也许不是最佳实践,但我认为这是一个非常有用的技巧。 ES6 modules have a single default export, as well as as many "regular" exports as you like. ES6模块具有单个default导出以及您喜欢的多个“常规”导出。 I leverage this to export the component multiple times. 我利用它多次导出组件。

The React convention is to use the default export when importing components (ie import MyComponent from './my-component' ). React约定是在导入组件时使用default导出(即import MyComponent from './my-component' )。 This means you can still export other things from the file. 这意味着您仍然可以从文件中导出其他内容。

My trick is to export default the HOC-wrapped component, so that you can use it in your source files as you would with any other component, but also export the unwrapped component as a "regular" component. 我的诀窍是export default的HOC包裹的组成部分,这样就可以像使用任何其他组件在源文件中使用它,而且出口展开的组件作为一个“普通”的组成部分。 That would look something like: 看起来像:

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

export default myHOCFunction()(MyComponent)

Then you can import the wrapped component with: 然后,您可以使用以下命令导入包装的组件:

import MyComponent from './my-component'

And the unwrapped component (ie for use in tests) with: 展开的组件(即用于测试)具有:

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

It's not the most explicit pattern in the world, but it is quite ergonomic imo. 这不是世界上最明确的模式,但它是相当符合人体工程学的imo。 If you wanted explicitness, you could do something like: 如果您希望明确,则可以执行以下操作:

export const WrappedMyComponent = myHOCFunction()(MyComponent)
export const UnwrappedMyComponent = MyComponent

You could use react-testing-library 's waitForElement to avoid having to explicitly await on your fetch call and simplify things a little: 您可以使用react-testing-librarywaitForElement来避免在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.");
  });
});

This will automatically wait for your element to appear and fail if it doesn't appear by some timeout. 这将自动等待您的元素出现,并且如果在一段时间内没有出现则失败。 You still have to await but hopefully this is a little closer to what you originally wanted. 您仍然需要await但是希望这与您最初想要的有点接近。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM