在 Promise 中调用 setState 时 React Jest 测试失败
React Jest Test Fails when setState Called in Promise
我正在尝试模拟 returns 承诺的服务,以便我可以验证是否使用正确的参数调用它。调用服务的方式因 state
而异,第一次调用服务会设置 state
.
在承诺中设置状态时,除非我将断言包装在 setTimeout
中或完全取消承诺,否则它不会更新。有没有一种方法可以通过简单的承诺和期望来做到这一点?
我的组件:
class App extends Component {
constructor(props) {
super(props);
this.state = {results: []};
this.service = props.service;
this.load = this.load.bind(this);
}
load() {
if (this.state.results.length === 0) {
this.service.load('state is empty')
.then(result => this.setState({results: result.data}));
} else {
this.service.load('state is nonempty')
.then(result => this.setState({results: result.data}));
}
}
render() {
return (
<div className="App">
<button id="submit" onClick={this.load}/>
</div>
);
}
}
我的测试:
it('Calls service differently based on results', () => {
const mockLoad = jest.fn((text) => {
return new Promise((resolve, reject) => {
resolve({data: [1, 2]});
});
});
const serviceStub = {load: mockLoad};
let component = mount(<App service={serviceStub}/>);
let button = component.find("#submit");
button.simulate('click');
expect(mockLoad).toBeCalledWith('state is empty');
button.simulate('click');
//this assertion fails as the state has not updated and is still 'state is empty'
expect(mockLoad).toBeCalledWith('state is nonempty');
});
如前所述,以下方法有效,但如果有解决方法,我宁愿不包装 expect:
setTimeout(() => {
expect(mockLoad).toBeCalledWith('state is nonempty');
done();
}, 50);
我还可以更改模拟函数的方式,以消除将起作用的承诺:
const mockLoad = jest.fn((text) => {
return {
then: function (callback) {
return callback({
data : [1, 2]
})
}
}
});
但我只想return一个承诺。
React batches setState
出于性能原因调用,所以此时
expect(mockLoad).toBeCalledWith('state is nonempty');
条件
if (this.state.results.length === 0) {
很可能仍然是 true
,因为 data
还没有 添加到 state
。
你最好的选择是
在第一个和第二个click event
之间使用forceUpdate。
或者将测试分成两个单独的部分,同时在测试之外提取通用逻辑。甚至 it
子句也会变得更具描述性,例如:it('calls service correctly when state is empty')
用于第一个测试,类似的用于第二个。
我赞成第二种方法。
setState() does not always immediately update the component. It may batch or defer the update until later.
阅读更多here。
将 Sinon 与 Sinon Stub Promise 结合使用,我能够让它发挥作用。存根 promise 库删除了 promise 的异步方面,这意味着 state
会及时更新渲染:
const sinon = require('sinon');
const sinonStubPromise = require('sinon-stub-promise');
sinonStubPromise(sinon);
it('Calls service differently based on results', () => {
const mockLoad = jest.fn((text) => {
return sinon.stub().returnsPromise().resolves({data: [1, 2]})();
});
const serviceStub = {load: mockLoad};
let component = mount(<App service={serviceStub}/>);
let button = component.find("#submit");
button.simulate('click');
expect(mockLoad).toBeCalledWith('state is empty');
button.simulate('click');
expect(mockLoad).toBeCalledWith('state is nonempty');
});
参见:
我正在尝试模拟 returns 承诺的服务,以便我可以验证是否使用正确的参数调用它。调用服务的方式因 state
而异,第一次调用服务会设置 state
.
在承诺中设置状态时,除非我将断言包装在 setTimeout
中或完全取消承诺,否则它不会更新。有没有一种方法可以通过简单的承诺和期望来做到这一点?
我的组件:
class App extends Component {
constructor(props) {
super(props);
this.state = {results: []};
this.service = props.service;
this.load = this.load.bind(this);
}
load() {
if (this.state.results.length === 0) {
this.service.load('state is empty')
.then(result => this.setState({results: result.data}));
} else {
this.service.load('state is nonempty')
.then(result => this.setState({results: result.data}));
}
}
render() {
return (
<div className="App">
<button id="submit" onClick={this.load}/>
</div>
);
}
}
我的测试:
it('Calls service differently based on results', () => {
const mockLoad = jest.fn((text) => {
return new Promise((resolve, reject) => {
resolve({data: [1, 2]});
});
});
const serviceStub = {load: mockLoad};
let component = mount(<App service={serviceStub}/>);
let button = component.find("#submit");
button.simulate('click');
expect(mockLoad).toBeCalledWith('state is empty');
button.simulate('click');
//this assertion fails as the state has not updated and is still 'state is empty'
expect(mockLoad).toBeCalledWith('state is nonempty');
});
如前所述,以下方法有效,但如果有解决方法,我宁愿不包装 expect:
setTimeout(() => {
expect(mockLoad).toBeCalledWith('state is nonempty');
done();
}, 50);
我还可以更改模拟函数的方式,以消除将起作用的承诺:
const mockLoad = jest.fn((text) => {
return {
then: function (callback) {
return callback({
data : [1, 2]
})
}
}
});
但我只想return一个承诺。
React batches setState
出于性能原因调用,所以此时
expect(mockLoad).toBeCalledWith('state is nonempty');
条件
if (this.state.results.length === 0) {
很可能仍然是 true
,因为 data
还没有 添加到 state
。
你最好的选择是
在第一个和第二个
click event
之间使用forceUpdate。或者将测试分成两个单独的部分,同时在测试之外提取通用逻辑。甚至
it
子句也会变得更具描述性,例如:it('calls service correctly when state is empty')
用于第一个测试,类似的用于第二个。
我赞成第二种方法。
setState() does not always immediately update the component. It may batch or defer the update until later.
阅读更多here。
将 Sinon 与 Sinon Stub Promise 结合使用,我能够让它发挥作用。存根 promise 库删除了 promise 的异步方面,这意味着 state
会及时更新渲染:
const sinon = require('sinon');
const sinonStubPromise = require('sinon-stub-promise');
sinonStubPromise(sinon);
it('Calls service differently based on results', () => {
const mockLoad = jest.fn((text) => {
return sinon.stub().returnsPromise().resolves({data: [1, 2]})();
});
const serviceStub = {load: mockLoad};
let component = mount(<App service={serviceStub}/>);
let button = component.find("#submit");
button.simulate('click');
expect(mockLoad).toBeCalledWith('state is empty');
button.simulate('click');
expect(mockLoad).toBeCalledWith('state is nonempty');
});
参见: