xavier.seignard
xavier.seignard

Reputation: 11144

Event is not catched in my test with stubbed function

I'm running a problem I don't get. The event I emit is not catched in my test. Here is the following code (event.js):

var util = require('util'),
    proc = require('child_process'),
    EventEmitter = require('events').EventEmitter;

var Event = function() {
    var _self = this;
    proc.exec('ls -l', function(error, stdout, stderr) {
        _self.emit('test');
        console.log('emitted');
    });
};
util.inherits(Event, EventEmitter);

module.exports = Event;

And the according test:

var proc = require('child_process'),
    sinon = require('sinon'),
    chai = require('chai'),
    expect = chai.expect,
    Event = require('./event'),
    myEvent, exec;

var execStub = function() {
    var _self = this;
    return sinon.stub(proc, 'exec', function(cmd, callback) {
        _self.cmd = cmd;
        console.log(cmd);
        callback();
    });
};

describe('Event', function() {
    beforeEach(function(){
        exec = execStub();
    });

    afterEach(function(){
        exec.restore();
    });

    it('Event should be fired', function(done) {
        myEvent = new Event();
        myEvent.on('test', function() {
            expect(exec.cmd).to.equal('ls -l');
            done();
        });
    });
});

For now, here is what I see:

But the test fails with a timeout, with that error message:

~ % mocha --timeout 15000 -R spec event.test.js


  Event
    ◦ Event should be fired: ls -l
emitted
    1) Event should be fired


  0 passing (15 seconds)
  1 failing

  1) Event Event should be fired:
     Error: timeout of 15000ms exceeded
      at null.<anonymous> (/usr/lib/node_modules/mocha/lib/runnable.js:165:14)
      at Timer.listOnTimeout [as ontimeout] (timers.js:110:15)

And if I remove the stub from my test, the test runs OK. And if i increase the timeout I still have the same problem.

Any idea of what I'm doing wrong?

Regards

Upvotes: 1

Views: 1383

Answers (1)

Miroslav Bajtoš
Miroslav Bajtoš

Reputation: 10785

Your stub changed the sync/async aspect of process.exec().

The internal Node's implementation guarantees that the callback is always run in the next turn of an event loop:

myEvent = new Event(); // calls process.exec
myEvent.on('test', function() {
    expect(exec.cmd).to.equal('ls -l');
    done();
});
// process.exec callback will be called after all this code is executed

Your stub is calling the callback immediately:

myEvent = new Event(); // calls process.exec
// process.exec callback is called immediately
// test event is emitted before listeners are attached
myEvent.on('test', function() {
  expect(exec.cmd).to.equal('ls -l');
  done();
});

The solution is process.nextTick():

var execStub = function() {
  var _self = this;
  return sinon.stub(proc, 'exec', function(cmd, callback) {
      _self.cmd = cmd;
      console.log(cmd);
      process.nextTick(callback);
  });
};

Your test has another problem: _self in the exec stub callback is referring to the global object, you are saving the value to global.cmd. You are expecting to have the value in exec.cmd in the test later.

Here is the final & fixed version of execStub:

var execStub = function() {
    var _self = sinon.stub(proc, 'exec', function(cmd, callback) {
        _self.cmd = cmd;
        console.log(cmd);
        process.nextTick(callback);
    });
    return _self;
};

See this post for more information on callback asynchronicity.

Upvotes: 2

Related Questions