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



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

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

我为测试所做的转储。第一个是测试中的监听状态。第二个来自代码本身,它将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;

有趣的是,这个花了一点时间来追逐。


节点中的相关部分.js 关于事件循环、计时器和process.nextTick()的文档:

从技术上讲,process.nextTick()不是事件循环的一部分。相反,nextTickQueue将在当前操作完成后进行处理,而不考虑事件循环的当前阶段。

。每当在给定阶段调用process.nextTick()时,传递给process.nextTick()的所有回调都将在事件循环继续之前解析。

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

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

文档警告说:

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

。事实证明,您也可以饿死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()回调最终会在将state.error设置为'my error'Promise回调之前运行。


正因为如此,文档建议如下:

我们建议开发人员在所有情况下都使用setImmediate(),因为它更容易推理


如果将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();
});
});
});

更新状态需要多个异步调用,因此process.nextTick()是不够的。要更新状态,需要执行以下操作:

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

简而言之,在断言状态之前,您需要等待更长的时间。

在没有嵌套process.nextTick()调用的情况下"等待"的一个有用习惯是定义测试帮助程序

function wait() {
return new Promise((resolve) => setTimeout(resolve));
}

然后做

await wait();

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

test(async () => {
})

而不是

test(done => {
})

最新更新