1

I am learning express and sinon (the hard way :) ). I'd like to unit test the following code:

//  ./routers/root.js

var express = require('express');
var router = express.Router();

router
  .route('/')
  .get(function(req, res) {
    res.writeHead(200, {'Content-Type': 'application/json'});
    res.write(JSON.stringify({Application: "NodeTours", Version: "2.0.0", Host: host, Port: port}));
    res.end();
  });

module.exports = router;

I've been trying different things without success. I'm back to the basic example. I realize it may seem like a complete newbie question but I'm clueless. Here's the latest try:

//   ./test/test.js

var sinon = require('sinon')
var expect = require('chai').expect

describe('Routers', function() {

  context('/', function() {
    var rootRouter = require('../routers/root')
    var res = {Application: "NodeTours", Version: "2.0.0", Host: 'host', Port: 'port'};
    it('should call / router', function() {
      rootRouter.route('/').get(null, res);
      // TODO: expect...
    })
  })
})

It fails with:

  Routers
    /
      1) should call / router


  0 passing (4ms)
  1 failing

  1) Routers
       /
         should call / router:
     Error: Route.get() requires a callback function but got a [object Null]
      at Route.<computed> [as get] (node_modules/express/lib/router/route.js:202:15)
      at Context.<anonymous> (test/test.js:10:29)
      at processImmediate (internal/timers.js:456:21)

I've tried multiple tutorials without success. I'd appreciate any pointers to how my code could be unit tested. Or how to adjust my code so unit testing is easier/possible.

My main app server.js file for context and reference (removed code not relevant for this question)

//   ./server.js
// npm pacakages
var express = require('express');
var bodyParser = require('body-parser');

// app imports
var { rootRouter } = require('./routers');
// other cut out

//globals
app = express();
app.use(bodyParser.json());
app.use('/', rootRouter);
// other cut out

// Set up Mongo DB connection
// code to st up db connection

// Start server (only if connection to DB established)
var server = MongoClient(url, { useUnifiedTopology: true, poolSize: 10 }).connect().then(client => {
  const dbo = client.db(dbName);

  // some init code cut out

  // make connection available
  app.locals.dbo = dbo;

  // start server
  app = app.listen(process.env.PORT || 7777, function () {
    host = require('os').hostname();
    port = "7777";
    logger.verbose("Startup: NodeTours listening at http://%s:%s", host, port)
  });
}).catch(error => {
  logger.error("Startup: Couldn't connect to the DB. The app will exit")
  logger.error("  Error: " + error)
  process.exit();
  }
);

Przemek
  • 203
  • 3
  • 10
  • Idk but if you want to test your webserver easily you can use the package [supertest](https://www.npmjs.com/package/supertest) to mock the server and send requests and verify the response easily. – Mickael B. Apr 29 '20 at 14:15
  • @MickaelB. I have used supertest successfully but I believe that would be classified as integration testing, not unit testing. – Przemek Apr 29 '20 at 14:20
  • Yes you are right – Mickael B. Apr 29 '20 at 14:30

1 Answers1

1

In your test case, you are attempting to call your GET handler directly:

it('should call / router', function() {
  rootRouter.route('/').get(null, res);
  // TODO: expect...
})

But appRouter.route('/').get() is a method intended to install a request handler, not to call it. That's why the error you are getting says:

Error: Route.get() requires a callback function but got a [object Null]

i.e., you are passing null for what you think is the request parameter, but which Express expects will be a method (callback) handler.

Normally what I will do is define my handlers as non-anonymous functions:

function rootHandler(req, res) {
    res.writeHead(200, {'Content-Type': 'application/json'});
    res.write(JSON.stringify({Application: "NodeTours", Version: "2.0.0", Host: host, Port: port}));
    res.end();
}

router
  .route('/')
  .get(rootHandler);

...and then unit tests can simply invoke the function directly, rather than go through Express (because normally we're interested in testing our own code, not the framework):

it('should do something with root handler', function() {
  rootHandler(null, res);
  // TODO: expect...
})

If you really want to test the routing logic, then using supertest as suggested in the comments is a good option.

Myk Willis
  • 12,306
  • 4
  • 45
  • 62
  • Incidentally, this post https://stackoverflow.com/a/48820849/925478 claims that Express supports invoking callback handlers with `app._router.handle`, but this doesn't seem to be documented and I have not personally used it. – Myk Willis Apr 30 '20 at 16:50
  • Good point on the handler. Thanks. That's what I did in my other routes - I separated routers from controllers/handlers e.g. ```var express = require('express'); var router = express.Router(); var cruisesController = require ("../controllers/cruises"); router .route('/') .get(cruisesController.cruises_get) router .route('/:id') .get(cruisesController.cruises_get_id) module.exports = router;``` – Przemek Apr 30 '20 at 17:12
  • I changed root.js to be ```var express = require('express'); var router = express.Router(); function rootHandler(req, res) { res.writeHead(200, {'Content-Type': 'application/json'}); res.write(JSON.stringify({Application: "NodeTours", Version: "2.0.0", Host: host, Port: port})); res.end(); } router .route('/') .get(rootHandler); module.exports = router; })``` – Przemek Apr 30 '20 at 17:16
  • and my test.js to be: ```var sinon = require('sinon') var expect = require('chai').expect describe('Routers', function() { context('/', function() { var rootRouter = require('../routers/root') var res = {Application: "NodeTours", Version: "2.0.0", Host: 'host', Port: 'port'}; it('should call / router', function() { rootRouter.rootHandler(null, res) }) }) }) ``` – Przemek Apr 30 '20 at 17:16
  • now I get ```Routers / 1) should call / router 0 passing (4ms) 1 failing 1) Routers / should call / router: TypeError: rootRouter.rootHandler is not a function at Context. (test/test.js:11:18) at processImmediate (internal/timers.js:456:21)``` – Przemek Apr 30 '20 at 17:17
  • Also, I used supertest to run test and no issues here. The difference is supertest starts the server etc. and run the test, so it's really an integration test. I wanted to be able to run unit tests for my modules/functions separately. – Przemek Apr 30 '20 at 17:19
  • Additionally, if I wanted to unit test conditions like that my root router does not respond to POST calls - how do I do this? – Przemek Apr 30 '20 at 17:22
  • @Przemek comments are difficult for this kind of conversation, but I think you've probably not exported `rootHandler` appropriately from your module. – Myk Willis Apr 30 '20 at 20:09