首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >问答首页 >在ReactJS中模拟带有状态更新的多个获取调用

在ReactJS中模拟带有状态更新的多个获取调用
EN

Stack Overflow用户
提问于 2019-03-04 13:36:44
回答 2查看 3.9K关注 0票数 3

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

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

我在测试时做过的。第一种是考试中听的状态。第二个是代码本身,它将state().error设置为从调用中接收的错误。

代码语言:javascript
运行
复制
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

实际测试代码:

代码语言:javascript
运行
复制
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();
    });
  });
});

这是我实际使用的组件:

代码语言:javascript
运行
复制
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;
EN

回答 2

Stack Overflow用户

回答已采纳

发布于 2019-03-14 03:13:40

有趣的是,这只花了一小段时间才找到它。

来自Node.js文档on process.nextTick()的相关部分

从技术上讲,process.nextTick()不是事件循环的一部分。相反,无论事件循环的当前阶段如何,都将在当前操作完成后处理nextTickQueue。 在给定阶段调用...any时,传递给process.nextTick()的所有回调都将在事件循环继续之前得到解决。

换句话说,节点一旦完成当前操作,就开始处理nextTickQueue,并且在继续执行事件循环之前,它将一直持续到队列为空。

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

医生警告说:

这可能会造成一些不好的情况,因为process.nextTick()允许您通过进行递归的 calls来“饿死”I/O,从而阻止事件循环到达轮询阶段。

事实证明,...and也可以饥饿您的Promise回调:

代码语言:javascript
运行
复制
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()的嵌套element.state()回调在之前运行Promise回调将state.error设置为'my error'

正因为如此,doc建议如下:

我们建议开发人员在所有情况下都使用setImmediate(),因为这样更容易解释

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

代码语言:javascript
运行
复制
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();
    });
  });
});
票数 2
EN

Stack Overflow用户

发布于 2019-03-06 17:10:28

更新状态需要几个异步调用,所以您的process.nextTick()是不够的。要更新状态,需要这样做:

  • 您的测试代码单击,事件处理程序回调排队。
  • 事件处理程序回调运行,运行fetch,获得承诺拒绝,并运行错误处理程序。
  • 错误处理程序运行setState,它将状态更新排队(setState是异步的!)
  • 测试代码运行,检查元素的状态
  • 状态更新运行。

简而言之,您需要等待更长时间才能断言状态。

在没有嵌套process.nextTick()调用的情况下“等待”的一个有用的成语是定义一个测试助手。

代码语言:javascript
运行
复制
function wait() {
    return new Promise((resolve) => setTimeout(resolve));
}

然后再做

代码语言:javascript
运行
复制
await wait();

测试代码中所要求的次数。请注意,这要求您将测试函数定义为

代码语言:javascript
运行
复制
test(async () => {

})

而不是

代码语言:javascript
运行
复制
test(done => {

})
票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/54984438

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档