reactjs - 在 ReactJS 中使用状态更新模拟多个获取调用

标签 reactjs jestjs

我有一个 ReactJS 组件,它做两件事: - 在 ComponentDidMount 上它将检索条目列表 - 在按钮上单击它会将选择的条目提交到后端

问题是我需要模拟两个请求(使用 fetch 发出)以便正确测试它。在我当前的测试用例中,我想测试按钮单击时提交的失败。然而,由于一些奇怪的原因,setState 被触发了,但是在我想比较它之后收到了来自它的更新。

我为测试做的转储。第一个是测试中 listen 的状态。第二个来自代码本身,它将 state().error 设置为从调用中收到的错误

FAIL  react/src/components/Authentication/DealerSelection.test.jsx (6.689s)
● Console

  console.log react/src/components/Authentication/DealerSelection.test.jsx:114
    { loading: true,
      error: null,
      options: [ { key: 22, value: 22, text: 'Stationstraat 5' } ] }
  console.log react/src/components/Authentication/DealerSelection.jsx:52
    set error to: my error

实际测试代码:

it('throws error message when dealer submit fails', done => {
  const mockComponentDidMount = Promise.resolve(
    new Response(JSON.stringify({"data":[{"key":22,"value":"Stationstraat 5"}],"default":22}), {
      status: 200,
      headers: { 'content-type': 'application/json' }
    })
  );
  const mockButtonClickFetchError = Promise.reject(new Error('my error'));

  jest.spyOn(global, 'fetch').mockImplementation(() => mockComponentDidMount);
  const element = mount(<DealerSelection />);

  process.nextTick(() => {
    jest.spyOn(global, 'fetch').mockImplementation(() => mockButtonClickFetchError);
    const button = element.find('button');
    button.simulate('click');
    process.nextTick(() => {
      console.log(element.state()); // state.error null even though it is set with setState but arrives just after this log statement
      global.fetch.mockClear();
      done();
    });
  });
});

这是我实际使用的组件:

import React, { Component } from 'react';
import { Form, Header, Select, Button, Banner } from '@omnius/react-ui-elements';
import ClientError from '../../Error/ClientError';
import { fetchBackend } from './service';
import 'whatwg-fetch';
import './DealerSelection.scss';

class DealerSelection extends Component {

  state = {
    loading: true,
    error: null,
    dealer: '',
    options: []
  }

  componentDidMount() {
    document.title = "Select dealer";

    fetchBackend(
      '/agent/account/dealerlist',
      {},
      this.onDealerListSuccessHandler,
      this.onFetchErrorHandler
    );
  }

  onDealerListSuccessHandler = json => {
    const options = json.data.map((item) => {
      return {
        key: item.key,
        value: item.key,
        text: item.value
      };
    });
    this.setState({
      loading: false,
      options,
      dealer: json.default
    });
  }

  onFetchErrorHandler = err => {
    if (err instanceof ClientError) {
      err.response.json().then(data => {
        this.setState({
          error: data.error,
          loading: false
        });
      });
    } else {
      console.log('set error to', err.message);
      this.setState({
        error: err.message,
        loading: false
      });
    }
  }

  onSubmitHandler = () => {
    const { dealer } = this.state;
    this.setState({
      loading: true,
      error: null
    });

    fetchBackend(
      '/agent/account/dealerPost',
      {
        dealer
      },
      this.onDealerSelectSuccessHandler,
      this.onFetchErrorHandler
    );
  }

  onDealerSelectSuccessHandler = json => {
    if (!json.error) {
      window.location = json.redirect; // Refresh to return back to MVC
    }
    this.setState({
      error: json.error
    });
  }

  onChangeHandler = (event, key) => {
    this.setState({
      dealer: event.target.value
    });
  }

  render() {
    const { loading, error, dealer, options } = this.state;
    const errorBanner = error ? <Banner type='error' text={error} /> : null;

    return (
      <div className='dealerselection'>
        <Form>
          <Header as="h1">Dealer selection</Header>
          { errorBanner }
          <Select
            label='My dealer'
            fluid
            defaultValue={dealer}
            onChange={this.onChangeHandler}
            maxHeight={5}
            options={options}
          />
          <Button
            primary
            fluid
            onClick={this.onSubmitHandler}
            loading={loading}
          >Select dealer</Button>
        </Form>
      </div>
    );
  }
}

export default DealerSelection;

最佳答案

有点意思,这只追了一小会儿


来自 Event Loop, Timers, and process.nextTick() 上 Node.js 文档的相关部分:

process.nextTick() is not technically part of the event loop. Instead, the nextTickQueue will be processed after the current operation is completed, regardless of the current phase of the event loop.

...any time you call process.nextTick() in a given phase, all callbacks passed to process.nextTick() will be resolved before the event loop continues.

换句话说,一旦当前操作完成,Node 就开始处理 nextTickQueue,并且它将继续直到队列为空,然后再继续事件循环。

这意味着如果 process.nextTick()nextTickQueue 正在处理时被调用,回调将被添加到队列中,并且它会在之前被处理事件循环继续

文档警告:

This can create some bad situations because it allows you to "starve" your I/O by making recursive process.nextTick() calls, which prevents the event loop from reaching the poll phase.

...事实证明,您也可以让 Promise 回调饿死:

test('Promise and process.nextTick order', done => {
  const order = [];
  
  Promise.resolve().then(() => { order.push('2') });
  
  process.nextTick(() => {
    Promise.resolve().then(() => { order.push('7') });
    order.push('3');  // this runs while processing the nextTickQueue...
    process.nextTick(() => {
      order.push('4');  // ...so all of these...
      process.nextTick(() => {
        order.push('5');  // ...get processed...
        process.nextTick(() => {
          order.push('6');  // ...before the event loop continues...
        });
      });
    });
  });

  order.push('1');

  setTimeout(() => {
    expect(order).toEqual(['1','2','3','4','5','6','7']);  // ...and 7 gets added last
    done();
  }, 0);
});

因此在这种情况下,记录 element.state() 的嵌套 process.nextTick() 回调最终在 之前运行>Promise 回调会将 state.error 设置为 'my error'


正因为如此,文档推荐如下:

We recommend developers use setImmediate() in all cases because it's easier to reason about


如果您将 process.nextTick 调用更改为 setImmediate(并将您的 fetch 模拟创建为 函数,那么Promise.reject() 不会立即运行并导致错误)然后你的测试应该按预期工作:

it('throws error message when dealer submit fails', done => {
  const mockComponentDidMount = () => Promise.resolve(
    new Response(JSON.stringify({"data":[{"key":22,"value":"Stationstraat 5"}],"default":22}), {
      status: 200,
      headers: { 'content-type': 'application/json' }
    })
  );
  const mockButtonClickFetchError = () => Promise.reject(new Error('my error'));

  jest.spyOn(global, 'fetch').mockImplementation(mockComponentDidMount);
  const element = mount(<DealerSelection />);

  setImmediate(() => {
    jest.spyOn(global, 'fetch').mockImplementation(mockButtonClickFetchError);
    const button = element.find('button');
    button.simulate('click');
    setImmediate(() => {
      console.log(element.state()); // state.error is 'my error'
      global.fetch.mockClear();
      done();
    });
  });
});

关于reactjs - 在 ReactJS 中使用状态更新模拟多个获取调用,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/54984438/

相关文章:

javascript - 如何在 jest/enzyme 测试中测试 axios get 请求函数?

reactjs - 使用 GatsbyJS 和 Material-UI 重新加载页面时样式消失

javascript - 测试覆盖率 React, Istanbul 尔 -_registerComponent(...) : Target container is not a DOM element

reactjs - 如何使用 jest 测试 jsx 组件?

javascript - 如何用 enzyme 测试包裹的组件?

unit-testing - Jest 单元测试中的全局变量

javascript - 模拟点击事件并使用 toBeCalled() 出现不是函数错误的错误

javascript - 如何在 reactjs 中以编程方式修改 dom 元素?

javascript - 为 React 模板配置 browserify

javascript - 未找到带有 svg 的 gatsby-background-image