【问题标题】:Why does my test finish before my (enzyme simulated event) synchronous event handler?为什么我的测试在我的(酶模拟事件)同步事件处理程序之前完成?
【发布时间】:2017-08-29 09:12:44
【问题描述】:

我有一个基于 mocha 的测试,它在我的 React 组件的基于 jsdom 的酶测试中在我的 onChange 处理程序之前完成,尽管该处理程序是使用 babel+ES2017 同步的。如果我做一个 1mssetTimeout() 来输入我的expect() 电话;测试通过

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

没有setTimeout()sinonlolex是否可以解决,如果不能; simon / lolex 可以吗?

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

测试输出

</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. ");
}

...

<form method="POST" onSubmit={this.handleSubmit} ref={(form) => {this.form = form;} } id="signupForm">

【问题讨论】:

    标签: javascript reactjs mocha.js enzyme jsdom


    【解决方案1】:

    我的第一个答案集中在 simulate 的异步性质上,但从 cmets 可以清楚地看出酶对该方法 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 返回一个承诺,await 将暂停函数的执行,直到该承诺得到解决。同时,在调用handleSubmit 之后继续执行任何代码。在这种情况下,这意味着您的测试将继续,并最终执行:

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

    ...失败了。表明挂起的 Ajax 请求有响应的事件可能已经到达事件队列,但只有在当前执行的代码完成后才会处理它。所以测试失败后,fetch 返回的承诺得到解决。这是因为fetch 的内部实现有一个回调通知响应已经到达,因此它解决了promise。这使得函数handleSubmit “唤醒”,因为await 现在解除了对执行的阻塞。

    还有第二个await 用于获取 JSON,这将再次引入事件队列循环。最终(不是双关语),代码将恢复并执行测试正在寻找的状态:

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

    所以...为了使测试成功,它必须实现一个异步回调,等待足够长的时间让 Ajax 调用得到响应。

    这可以通过setTimeout(done, ms) 来完成,其中ms 应该是足够长的毫秒数,足以确保Ajax 响应可用。

    【讨论】:

    • 我猜为什么这对我来说并不明显,是表单提交之前的所有输入更改事件都及时成功呈现。所以听起来工作的临界质量刚刚达到它在测试“之后”完成的地步?
    • 确实,这里的“异步”意味着事件被放入事件队列中,因此在当前运行的代码(你的测试)完成之前它不会被处理(即JS调用堆栈)是空的)。在处理事件(在事件队列中)之后完成某事的唯一方法是将某事也放入该队列中。这就是setTimeout 所做的。
    • 也许酶的作用不同。 simulate() 对我来说似乎是同步的或可靠的。我设置状态(通过事件处理程序)10000 次,最后一次不同的状态。该状态在不使用setTimeout() 的情况下呈现正常。但是一旦事件处理程序有一些异步的东西(在我的例子中是模拟的fetch()),它就会在下一个循环中呈现。我将用一个示例组件+测试来回答我自己的帖子,这似乎向我说明了这一点。除非您不同意或有进一步的补充?
    • 请继续。从我引用的文章中,我了解到 simulate 是异步的,但显然你对它有不同的行为。
    • 其实我也会重写我的答案。
    【解决方案2】:

    在我看来,与ReactTestUtils(@trincot 的答案所基于)不同,酶的simulate() 实际上是同步的。然而,我对fetch() 的模拟调用是异步,并且承诺正在下一个事件循环中解决。将期望或断言包装在 setTimeout(()=&gt;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();
        });
      });
     });
    

    【讨论】:

      猜你喜欢
      • 2016-01-19
      • 2017-03-28
      • 1970-01-01
      • 2015-08-29
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      相关资源
      最近更新 更多