在 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');
});

参见:

http://sinonjs.org/

https://github.com/substantial/sinon-stub-promise