为什么我的测试在我的(酶模拟事件)同步事件处理程序之前完成?

时间:2017-04-04 09:02:05

标签: javascript reactjs mocha enzyme jsdom

我有一个基于mocha的测试,它在我的onChange处理程序之前在我的React组件的基于jsdom的酶测试中完成,尽管该处理程序使用babel + ES2017同步。如果我执行<{1>} 1ms 来将setTimeout()个来电话放入;测试传递

只是想知道故障在哪里?我确定这里有一些简单的概念,我不考虑。我认为jsdom或酶不会等待事件处理程序完成?使用expect()模拟fetch()的时间长度加剧了一个问题(因为它通常是异步的)。

是否可以在没有fetch-mocksetTimeout()sinon的情况下解析,如果没有,可以使用lolex / simon吗?

明天我希望我能重构它以避免在测试中模拟fetch()。

测试输出

lolex

自举

</div>
    1) flashes a nice message upon success
Success now!!
End of function now. 


10 passing (4s)
1 failing

 1) <Signup /> flashes a nice message upon success:
 Uncaught AssertionError: expected { Object (root, unrendered, ...) } to have a length of 1 but got 0
  at test/integration/jsx/components/signup.test.js:38:54
  at _combinedTickCallback (internal/process/next_tick.js:67:7)
  at process._tickDomainCallback (internal/process/next_tick.js:122:9)

...

require('babel-register')();
require('babel-polyfill');

测试

var jsdom = require('jsdom').jsdom;
var exposedProperties = ['window', 'navigator', 'document'];

global.document = jsdom('');
global.window = document.defaultView;
global.FormData = document.defaultView.FormData;
Object.keys(document.defaultView).forEach((property) => {
  if (typeof global[property] === 'undefined') {
    exposedProperties.push(property);
    global[property] = document.defaultView[property];
  }
});

global.navigator = {
  userAgent: 'node.js'
};

documentRef = document;

...

import React from 'react';
import { expect } from 'chai';
import { shallow, mount, render } from 'enzyme';
import Signup from '../../../../assets/js/components/signup.jsx';
import fetchMock from 'fetch-mock';
import sinon from 'sinon';
import 'isomorphic-fetch';

组件

it("flashes a nice message upon success", function(){
  fetchMock.mock("*", {body: {}});
  const wrapper = shallow(<Signup />);

  wrapper.find('#email').simulate('change', {target: {id: 'email', value: validUser.email}});

  const signupEvent = {preventDefault: sinon.spy()};

  wrapper.find('#signupForm').simulate('submit', signupEvent);
  wrapper.update();

  console.log(wrapper.debug());
  expect(signupEvent.preventDefault.calledOnce).to.be.true;
  expect(wrapper.find('.alert-success')).to.have.length(1);
  expect(wrapper.find('.alert-success').text()).to.contain('Your sign up was successful!');

  fetchMock.restore();
});

...

async handleSubmit(e) {
  e.preventDefault();
  this.setState({ type: 'info', message: 'Sending...', shouldValidateForm: true });
  let form = new FormData(this.form);
  let response;
  let responseJson = {};
  try {
    response = await fetch("/signup", {
      method: "POST",
      body: form
    });
    responseJson = await response.json();
    if(!response.ok){
      throw new Error("There was a non networking error. ");
    }
    this.setState({ type: 'success', message: 'Your sign up was successful!' });
    console.log("Success now!!");
  } catch(err) {
    this.setState({ type: 'danger', message: "There was a technical problem. "});
  }
  console.log("End of function now. ");
}

2 个答案:

答案 0 :(得分:4)

我的第一个答案集中在simulate的异步性质上,但是从评论中可以看出,该方法is not asynchronous的酶的实现很明显,因为它只是同步调用点击处理程序。所以这是对我的回答的重写,重点是异步行为的其他原因。

这个测试:

expect(wrapper.find('.alert-success')).to.have.length(1);

...失败,因为那时还没有执行以下行:

this.setState({ type: 'success', message: 'Your sign up was successful!' });

我在此假设此setState调用会将alert-success类添加到消息元素中。

要了解为何尚未设置此状态,请考虑执行流程:

wrapper.find('#signupForm').simulate('submit', signupEvent);

这将触发表单的onsubmit属性中指定的内容:

onSubmit={this.handleSubmit} 

所以调用了handleSubmit。然后设置状态:

this.setState({ type: 'info', message: 'Sending...', shouldValidateForm: true });

...但这不是您需要的状态:它不会添加alert-success类。然后进行Ajax调用:

response = await fetch("/signup", {
    method: "POST",
    body: form
});

fetch返回一个promise,await将暂停执行该函数,直到该promise被解决。与此同时,继续执行跟随<{1}}的调用后执行的任何代码。在这种情况下,这意味着您的测试继续进行,并最终执行:

handleSubmit

......失败了。发出挂起的Ajax请求有响应的事件可能已到达事件队列,但只会在当前执行的代码完成后处理。因此测试失败后,expect(wrapper.find('.alert-success')).to.have.length(1); 返回的承诺得到解决。这是因为fetch的内部实现具有回调通知响应已到达,因此它解析了承诺。这使得函数fetch&#34;唤醒&#34;,因为handleSubmit现在解锁执行。

获取JSON还有第二个await,它将再次引入一个事件队列周期。最终(双关语意图),代码将恢复并执行测试所寻找的状态:

await

所以......为了使测试成功,它必须实现异步回调,等待Ajax调用才能获得响应。

这可以使用this.setState({ type: 'success', message: 'Your sign up was successful!' }); 完成,其中setTimeout(done, ms)应该是毫秒级,足以确保Ajax响应可用。

答案 1 :(得分:1)

在我看来,与ReactTestUtils(@ trincot的答案基于)不同, simulate()实际上是同步。但是我对fetch()的模拟调用是异步,并且承诺在下一个事件循环中解析。在setTimeout(()=>done(), 0)中包含期望或断言应该足够并且可能比setImmediate()更可靠,setTimeout()似乎比<Example /> updated asynchronously onChangeError ran. SUCCESS SOON: Taking a break... Setting delayed success. ✓ has a rendered success message on the next event loop updated synchronously onChangeError ran. Setting success. ✓ has a rendered success message on this loop onChangeError ran. onChangeError ran. onChangeError ran. ... onChangeError ran. onChangeError ran. onChangeError ran. Setting success. ✓ has a rendered success message on this loop despite a large simulation workload (2545ms) 3 passing (6s) 具有更高的优先级(即使它们都可能在执行相同的事件循环)。

这是我写的一个组件和测试来演示。

测试输出

import React from 'react';
export default class Example extends React.Component {
  constructor(props){
    super(props);
    this.onChangeError = this.onChangeError.bind(this);
    this.onChangeSuccess = this.onChangeSuccess.bind(this);
    this.onChangeDelayedSuccess = this.onChangeDelayedSuccess.bind(this);
    this.state = { message: "Initial message. " };
  }
  onChangeError(e){
    console.log("onChangeError ran. ");
    this.setState({message: "Error: There was an error. "})
  }
  onChangeSuccess(e) {
    console.log("Setting success. ");
    this.setState({message: "The thing was a success!"});
  };
  onChangeDelayedSuccess(e){
    console.log('SUCCESS SOON: Taking a break...');
    setTimeout(() =>{
      console.log("Setting delayed success. ");
      this.setState({message: "The thing was a success!"});
    }, 0);
  }
  render(){
    return(
     <div>
       <p>{ this.state.message}</p>
       <input type="text" id="forceError" onChange={this.onChangeError} />
       <input type="text" id="forceSuccess" onChange={this.onChangeSuccess} />
       <input type="text" id="forceDelayedSuccess" onChange={this.onChangeDelayedSuccess} />
     </div>
    );
  }
}

组件

import React from 'react';
import { expect } from 'chai';
import { shallow, mount, render } from 'enzyme';
import Example from '../../../../assets/js/components/example.jsx';

describe("<Example />", function() {
  describe("updated asynchronously", function() {
    it("has a rendered success message on the next event loop ", function(done) {
      const wrapper = shallow(<Example />);
      wrapper.find('#forceError').simulate('change', {target: {value: ""}});
      wrapper.find('#forceDelayedSuccess').simulate('change', {target: {value: ""}});

      setTimeout(function(){
        expect(wrapper.find('p').text()).to.contain('The thing was a success!');
        done();
      }, 0);
    });
  });
  describe("updated synchronously", function(){
    it("has a rendered success message on this loop", function(done) {
      const wrapper = shallow(<Example />);
      wrapper.find('#forceError').simulate('change', {target: {value: ""}});
      wrapper.find('#forceSuccess').simulate('change', {target: {value: ""}});

      expect(wrapper.find('p').text()).to.contain('The thing was a success!');
      done();
    });
    it("has a rendered success message on this loop despite a large simulation workload", function(done) {
      this.timeout(100000);
      const wrapper = shallow(<Example />);
      for(var i=1; i<=10000;i++){
        wrapper.find('#forceError').simulate('change', {target: {value: ""}});
      }
      wrapper.find('#forceSuccess').simulate('change', {target: {value: ""}});

      expect(wrapper.find('p').text()).to.contain('The thing was a success!');
      done();
    });
  });
 });

测试

findComments : function(ne, sw, filter, timezoneOffset) {
    // ... do some Mongo queries and updates ... etc.  nothing too weird.

    console.log("returning now...");
    return result;
}