1

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:

  • the event is actually emitted during the test since the console.log('emitted'); occurs
  • the exec function is actually stubbed since the console.log(cmd); occurs

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

xavier.seignard
  • 11,254
  • 13
  • 51
  • 75

1 Answers1

2

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.

Miroslav Bajtoš
  • 10,667
  • 1
  • 41
  • 99
  • Nice! Great educational answer! Thanks for taking the time to write it down! – xavier.seignard Oct 10 '13 at 01:06
  • + 1 for great answer. I have a similar problem but I couldn't implement your solution :( Could you take a look @Miroslav ? Thank you! http://stackoverflow.com/questions/29133140/how-to-test-a-custom-module-running-node-fluent-ffmpeg-an-async-module – scaryguy Mar 19 '15 at 18:14