【问题标题】:How do I test async fetch in react componentDidMount with chained promises?如何使用链式承诺在 react componentDidMount 中测试异步获取?
【发布时间】:2018-12-13 00:51:40
【问题描述】:

我一直在尝试了解如何测试在componentDidMount 期间运行异步获取的已安装组件。

问题是我可以让它等待初始提取触发,而不是等待从承诺中解决所有链。

这是一个例子:

import React from "react";

class App extends React.Component {
  state = {
    groceries: [],
    errorStatus: ""
  };

  componentDidMount() {
    console.log("calling fetch");

    fetch("/api/v1/groceries")
      .then(this.checkStatus)
      .then(this.parseJSON)
      .then(this.setStateFromData)
      .catch(this.setError);
  }

  checkStatus = results => {
    if (results.status >= 400) {
      console.log("bad status");

      throw new Error("Bad Status");
    }

    return results;
  };

  setError = () => {
    console.log("error thrown");

    return this.setState({ errorStatus: "Error fetching groceries" });
  };

  parseJSON = results => {
    console.log("parse json");

    return results.json();
  };

  setStateFromData = data => {
    console.log("setting state");

    return this.setState({ groceries: data.groceries });
  };

  render() {
    const { groceries } = this.state;

    return (
      <div id="app">
        {groceries.map(grocery => {
          return <div key={grocery.id}>{grocery.item}</div>;
        })}
      </div>
    );
  }
}

export default App;

测试:

import Enzyme from 'enzyme';
import Adapter from 'enzyme-adapter-react-16';
import React from 'react';
import { mount } from 'enzyme'
import App from './App';

Enzyme.configure({ adapter: new Adapter() });

const mockResponse = (status, statusText, response) => {
  return new window.Response(response, {
    status: status,
    statusText: statusText,
    headers: {
      'Content-type': 'application/json'
    }
  });
};

describe('App', () => {
  describe('componentDidMount', () => {
    it('sets the state componentDidMount', async () => {
      console.log('starting test for 200')

      global.fetch = jest.fn().mockImplementation(() => Promise.resolve(
        mockResponse(
          200,
          null,
          JSON.stringify({
            groceries: [
              { item: 'nuts', id: 10 }, { item: 'greens', id: 3 }
            ]
          })
        )
      ));

      const renderedComponent = await mount(<App />)
      await renderedComponent.update()

      console.log('finished test for 200')
      expect(renderedComponent.state('groceries').length).toEqual(2)
    })

    it('sets the state componentDidMount on error', async () => {
      console.log('starting test for 500')

      window.fetch = jest.fn().mockImplementation(() => Promise.resolve(
        mockResponse(
          400,
          'Test Error',
          JSON.stringify({ status: 400, statusText: 'Test Error!' })
        )
      ))

      const renderedComponent = await mount(<App />)
      await renderedComponent.update()

      console.log('finished test for 500')
      expect(renderedComponent.state('errorStatus')).toEqual('Error fetching groceries')
    })
  })
})

当它运行时,我会收到以下控制台日志记录顺序(注意测试完成,然后记录状态已设置):

console.log src/App.test.js:22
  starting test for 200

console.log src/App.js:10
  calling fetch

console.log src/App.js:36
  parse json

console.log src/App.test.js:39
  finished test for 200

console.log src/App.js:42
  setting state

我已经为我的代码创建了一个示例沙箱:

这在我的应用程序中被抽象了很多,因此更改代码本身要困难得多(例如,我想在具有 redux 存储的更高组件上进行测试,而这个更低的组件调用 fetch,并设置最终通过 thunk 存储)。

这是如何测试的?

【问题讨论】:

    标签: javascript reactjs fetch jestjs enzyme


    【解决方案1】:

    Update 方法实际上没有返回 promise,这就是 await 不能正常工作的原因。 要修复单元测试,您可以将 fetch 调用移至另一个方法并从测试中使用该函数,以便 await 正常工作。

     componentDidMount() {
        console.log("calling fetch");
    
        this.fetchCall();
      }
    
      fetchCall() {
        return fetch("/api/v1/groceries")
          .then(this.checkStatus)
          .then(this.parseJSON)
          .then(this.setStateFromData)
          .catch(this.setError);
      }
    

    使用 instance() 访问 fetchCall 方法。

    const renderedComponent = mount(<App />);
    await renderedComponent.instance().fetchCall();
    

    我在codesandbox中修改了以上更改:https://codesandbox.io/s/k38m6y89o7

    【讨论】:

    • 谢谢!您的代码框链接似乎不起作用(代码未更改),但您的解释让我到了那里。这甚至在子级上方有父级时也有效:codesandbox.io/embed/kkok5j144v?previewwindow=tests认为 instance() 返回了父级,但它显然没有,实际上返回找到的组件的实例。
    • 这不会导致 fetch 调用被执行两次吗?靠谱吗?
    • fetch 调用将只执行一次,即来自 componentDidmount。如果您认为单元测试会再次调用它,那么它不会因为单元测试在您运行单元测试而不是在实际浏览器中运行。
    【解决方案2】:

    我不知道为什么await renderedComponent.update() 在这里没有帮助你(.update 没有返回 Promise,但它仍然意味着下面的所有内容都是单独的微任务)。

    但是将东西包装到setTimeout(..., 0) 对我有用。因此,微任务和宏任务之间的区别实际上以某种方式发生。

       it("sets the state componentDidMount on error", done => {
          console.log("starting test for 500");
    
          window.fetch = jest
            .fn()
            .mockImplementation(() =>
              Promise.resolve(
                mockResponse(
                  400,
                  "Test Error",
                  JSON.stringify({ status: 400, statusText: "Test Error!" })
                )
              )
            );
    
          const renderedComponent = mount(<App />);
          setTimeout(() => {
            renderedComponent.update();
    
            console.log("finished test for 500");
            expect(renderedComponent.state("errorStatus")).toEqual(
              "Error fetching groceries"
            );
            done();
          }, 0);
        });
      });
    

    这种方法的唯一缺点是:当expect() 失败时,它不会在 Jest 输出中显示失败消息。开玩笑只是抱怨测试没有在 5000 毫秒内完成。同时像Expected value to equal: ... 这样的有效错误消息会发送到控制台。

    【讨论】:

      猜你喜欢
      • 1970-01-01
      • 2020-02-12
      • 2016-11-13
      • 2020-01-20
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2021-10-03
      • 2020-07-30
      相关资源
      最近更新 更多