Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to mock http.ServerResponse and http.IncomingMessage for express.static

I've had no trouble testing my own route handlers but in this case I want to test express's static handler. I can't for the life of me figure out why it's hanging. Clearly there's some callback I'm missing or some event I need to emit.

I tried to make the smallest example I could.

var events = require('events');
var express = require('express');
var stream = require('stream');
var util = require('util');

function MockResponse(callback) {
  stream.Writable.call(this);
  this.headers = {};
  this.statusCode = -1;
  this.body = undefined;

  this.setHeader = function(key, value) {
    this.headers[key] = value;
  }.bind(this);

  this.on('finish', function() {
    console.log("finished response");
    callback();
  });
};

util.inherits(MockResponse, stream.Writable);

MockResponse.prototype._write = function(chunk, encoding, done) {
  if (this.body === undefined) {
    this.body = "";
  }
  this.body += chunk.toString(encoding !== 'buffer' ? encoding : undefined);
  done();
};

function createRequest(req) {
  var emitter = new events.EventEmitter();
  req.on = emitter.on.bind(emitter);
  req.once = emitter.once.bind(emitter);
  req.addListener = emitter.addListener.bind(emitter);
  req.emit = emitter.emit.bind(emitter);
  return req;
};

describe('test', function() {

  var app;

  before(function() {
    app = express();
    app.use(express.static(__dirname));
  });

  it('gets test.js', function(done) {

    var req = createRequest({
        url: "http://foo.com/test.js",
        method: 'GET',
        headers: {
        },
    });
    var res = new MockResponse(responseDone);
    app(req, res);

    function responseDone() {
      console.log("done");
      done();
    }

  });

});

Setup,

mkdir foo
cd foo
mkdir test
cat > test/test.js   # copy and paste code above
^D
npm install express
npm install mocha
node node_modules/mocha/bin/mocha --recursive

it just times out.

What am I missing?

I also tried making the request a Readable stream. No change

var events = require('events');
var express = require('express');
var stream = require('stream');
var util = require('util');

function MockResponse(callback) {
  stream.Writable.call(this);
  this.headers = {};
  this.statusCode = -1;
  this.body = undefined;

  this.setHeader = function(key, value) {
    this.headers[key] = value;
  }.bind(this);

  this.on('finish', function() {
    console.log("finished response");
    callback();
  });
};

util.inherits(MockResponse, stream.Writable);

MockResponse.prototype._write = function(chunk, encoding, done) {
  if (this.body === undefined) {
    this.body = "";
  }
  this.body += chunk.toString(encoding !== 'buffer' ? encoding : undefined);
  done();
};

function MockMessage(req) {
  stream.Readable.call(this);
  var self = this;
  Object.keys(req).forEach(function(key) {
    self[key] = req[key];
  });
}

util.inherits(MockMessage, stream.Readable);

MockMessage.prototype._read = function() {
  this.push(null);
};


describe('test', function() {

  var app;

  before(function() {
    app = express();
    app.use(express.static(__dirname));
  });

  it('gets test.js', function(done) {

    var req = new MockMessage({
        url: "http://foo.com/test.js",
        method: 'GET',
        headers: {
        },
    });
    var res = new MockResponse(responseDone);
    app(req, res);

    function responseDone() {
      console.log("done");
      done();
    }

  });

});

I've still been digging. Look inside static-server I see it creates a Readable stream by calling fs.createReadStream. It does effectively

var s = fs.createReadStream(filename);
s.pipe(res);

So trying that myself works just fine

  it('test stream', function(done) {
    var s = fs.createReadStream(__dirname + "/test.js");
    var res = new MockResponse(responseDone);
    s.pipe(res);

    function responseDone() {
      console.log("done");
      done();
    }    
  });

I thought maybe it's something about express waiting for the input stream to finish but that doesn't seem to be it either. If I consume the mock input stream with the response it works just fine

  it('test msg->res', function(done) {
    var req = new MockMessage({});
    var res = new MockResponse(responseDone);
    req.pipe(res);

    function responseDone() {
      console.log("done");
      done();
    }    
  });

Any insight what I might be missing would be helpful

Note: while suggestions for 3rd party mocking libraries are appreciated I'm still really looking to understand what I'm missing to do it myself. Even if I eventually switch to some library I still want to know why this isn't working.

like image 724
gman Avatar asked Sep 16 '15 01:09

gman


2 Answers

I found two issues that prevent the finish callback from being executed.

  1. serve-static uses send module which is used to create file readstream from the path and pipe it to res object. But that module uses on-finished module which checks if finished attribute is set to false in response object, otherwise it destroys the file readstream. So filestream never gets a chance to emit data event.

  2. express initialization overwrites the response object prototype. So the default stream methods like end() method is overwritten by http response prototype:

    exports.init = function(app){
      return function expressInit(req, res, next){
        ...
        res.__proto__ = app.response;
        ..
      };
    };
    

    To prevent this, I added another middleware right before static middleware to reset it back to MockResponse prototype:

    app.use(function(req, res, next){
      res.__proto__ = MockResponse.prototype; //change it back to MockResponse prototype
      next();
    });
    

Here are the changes made to make it work with MockResponse:

...
function MockResponse(callback) {
  ...
  this.finished = false; // so `on-finished` module doesn't emit finish event prematurely

  //required because of 'send' module
  this.getHeader = function(key) {
    return this.headers[key];
  }.bind(this);
  ...
};

...
describe('test', function() {

  var app;

  before(function() {
    app = express();

    //another middleware to reset the res object
    app.use(function(req, res, next){
      res.__proto__ = MockResponse.prototype;
      next();
    });

    app.use(express.static(__dirname));
  });

  ...

});

EDIT:

As @gman pointed out, it is possible to use direct property instead of prototype method. In that case the extra middleware to overwrite prototype isn't necessary:

function MockResponse(callback) {
  ...
  this.finished = false; // so `on-finished` module doesn't emit finish event prematurely

  //required because of 'send' module
  this.getHeader = function(key) {
     return this.headers[key];
  }.bind(this);

  ...

  //using direct property for _write, write, end - since all these are changed when prototype is changed
  this._write = function(chunk, encoding, done) {
    if (this.body === undefined) {
      this.body = "";
    }
    this.body += chunk.toString(encoding !== 'buffer' ? encoding : undefined);
    done();
  };

  this.write = stream.Writable.prototype.write;
  this.end = stream.Writable.prototype.end;

};
like image 152
hassansin Avatar answered Nov 13 '22 14:11

hassansin


It appears my answer is not complete. For some reason the app works only if the file is not found. First thing to debug is do the following in your shell (or cmd):

export DEBUG=express:router,send

then run the test, you'll get more info.

Meanwhile I am still looking into this, for now, ignore my answer below.

----------- ignore this till I verify that it does work -----------

It seems like express static does not favor the absolute path you give it (__dirname).

Try:

app.use(express.static('.'));

and it will work. Note that your current dir for the mocha runner is 'test/'

I have to admit this is quite a mistery. I tried 'fulling' it by doing:

app.use(express.static(__dirname + '/../test')

but still it didn't work. Even specifying a full path did not solve this. Strange.

like image 28
Meir Avatar answered Nov 13 '22 14:11

Meir