How does one unit test routes with Express?

node.jsExpress

node.js Problem Overview


I'm in the process of learning Node.js and have been playing around with Express. Really like the framework;however, I'm having trouble figuring out how to write a unit/integration test for a route.

Being able to unit test simple modules is easy and have been doing it with Mocha; however, my unit tests with Express fail since the response object I'm passing in doesn't retain the values.

Route-Function Under Test (routes/index.js):

exports.index = function(req, res){
  res.render('index', { title: 'Express' })
};

Unit Test Module:

var should = require("should")
	, routes = require("../routes");

var request = {};
var response = {
	viewName: ""
	, data : {}
	, render: function(view, viewData) {
		viewName = view;
		data = viewData;
	}
};

describe("Routing", function(){
	describe("Default Route", function(){
        it("should provide the a title and the index view name", function(){
		routes.index(request, response);
		response.viewName.should.equal("index");
	    });

	});
});

When I run this, it fails for "Error: global leaks detected: viewName, data".

  1. Where am I going wrong so that I can get this working?

  2. Is there a better way for me to unit test my code at this level?

Update

  1. Corrected code snippet since I initially forgot "it()".

node.js Solutions


Solution 1 - node.js

As others have recommended in comments, it looks like the canonical way to test Express controllers is through supertest.

An example test might look like this:

describe('GET /users', function(){
  it('respond with json', function(done){
    request(app)
      .get('/users')
      .set('Accept', 'application/json')
      .expect(200)
      .end(function(err, res){
        if (err) return done(err);
        done()
      });
  })
});

Upside: you can test your entire stack in one go.

Downside: it feels and acts a bit like integration testing.

Solution 2 - node.js

I've come to the conclusion that the only way to really unit test express applications is to maintain a lot of separation between the request handlers and your core logic.

Thus, your application logic should be in separate modules that can be required and unit tested, and have minimal dependence on the Express Request and Response classes as such.

Then in the request handlers you need to call appropriate methods of your core logic classes.

I'll put an example up once I've finished restructuring my current app!

I guess something like this? (Feel free to fork the gist or comment, I'm still exploring this).

Edit

Here's a tiny example, inline. See the gist for a more detailed example.

/// usercontroller.js
var UserController = {
   _database: null,
   setDatabase: function(db) { this._database = db; },
   
   findUserByEmail: function(email, callback) {
       this._database.collection('usercollection').findOne({ email: email }, callback);
   }
};

module.exports = UserController;

/// routes.js

/* GET user by email */
router.get('/:email', function(req, res) {
    var UserController = require('./usercontroller');
    UserController.setDB(databaseHandleFromSomewhere);
    UserController.findUserByEmail(req.params.email, function(err, result) {
        if (err) throw err;
        res.json(result);
    });
});

Solution 3 - node.js

Change your response object:

var response = {
    viewName: ""
    , data : {}
    , render: function(view, viewData) {
        this.viewName = view;
        this.data = viewData;
    }
};

And it will work.

Solution 4 - node.js

The easiest way to test HTTP with express is to steal TJ's http helper

I personally use his helper

it("should do something", function (done) {
    request(app())
    .get('/session/new')
    .expect('GET', done)
})

If you want to specifically test your routes object, then pass in correct mocks

describe("Default Route", function(){
    it("should provide the a title and the index view name", function(done){
        routes.index({}, {
            render: function (viewName) {
                viewName.should.equal("index")
                done()
            }
        })
    })
})

Solution 5 - node.js

if unit testing with express 4 note this example from gjohnson :

var express = require('express');
var request = require('supertest');
var app = express();
var router = express.Router();
router.get('/user', function(req, res){
  res.send(200, { name: 'tobi' });
});
app.use(router);
request(app)
  .get('/user')
  .expect('Content-Type', /json/)
  .expect('Content-Length', '15')
  .expect(200)
  .end(function(err, res){
    if (err) throw err;
  });

Solution 6 - node.js

To achieve unit testing instead of integration testing, I mocked the response object of the request handler.

/* app.js */
import endpointHandler from './endpointHandler';
// ...
app.post('/endpoint', endpointHandler);
// ...

/* endpointHandler.js */
const endpointHandler = (req, res) => {
  try {
    const { username, location } = req.body;

    if (!(username && location)) {
      throw ({ status: 400, message: 'Missing parameters' });
    }

    res.status(200).json({
      location,
      user,
      message: 'Thanks for sharing your location with me.',
    });
  } catch (error) {
    console.error(error);
    res.status(error.status).send(error.message);
  }
};

export default endpointHandler;

/* response.mock.js */
import { EventEmitter } from 'events';

class Response extends EventEmitter {
  private resStatus;

  json(response, status) {
    this.send(response, status);
  }

  send(response, status) {
    this.emit('response', {
      response,
      status: this.resStatus || status,
    });
  }

  status(status) {
    this.resStatus = status;
    return this;
  }
}

export default Response;

/* endpointHandler.test.js */
import Response from './response.mock';
import endpointHandler from './endpointHander';

describe('endpoint handler test suite', () => {
  it('should fail on empty body', (done) => {
    const res = new Response();

    res.on('response', (response) => {
      expect(response.status).toBe(400);
      done();
    });

    endpointHandler({ body: {} }, res);
  });
});

Then, to achieve integration testing, you can mock your endpointHandler and call the endpoint with supertest.

Solution 7 - node.js

I was wondering this as well, but specifically for unit tests and not integration tests. This is what I'm doing right now,

test('/api base path', function onTest(t) {
  t.plan(1);

  var path = routerObj.path;

  t.equals(path, '/api');
});


test('Subrouters loaded', function onTest(t) {
  t.plan(1);

  var router = routerObj.router;

  t.equals(router.stack.length, 5);
});

Where the routerObj is just {router: expressRouter, path: '/api'}. I then load in subrouters with var loginRouterInfo = require('./login')(express.Router({mergeParams: true})); and then the express app calls an init-function taking in the express router as a parameter. The initRouter then calls router.use(loginRouterInfo.path, loginRouterInfo.router); to mount the subrouter.

The subrouter can be tested with:

var test = require('tape');
var routerInit = require('../login');
var express = require('express');
var routerObj = routerInit(express.Router());

test('/login base path', function onTest(t) {
  t.plan(1);

  var path = routerObj.path;

  t.equals(path, '/login');
});


test('GET /', function onTest(t) {
  t.plan(2);

  var route = routerObj.router.stack[0].route;

  var routeGetMethod = route.methods.get;
  t.equals(routeGetMethod, true);

  var routePath = route.path;
  t.equals(routePath, '/');
});

Solution 8 - node.js

In my case the only I wanted to test is if the right handler has been called. I wanted to use supertest to laverage the simplicity of making the requests to the routing middleware. I am using Typescript a and this is the solution that worked for me

// ProductController.ts

import { Request, Response } from "express";

class ProductController {
  getAll(req: Request, res: Response): void {
    console.log("this has not been implemented yet");
  }
}
export default ProductController

The routes

// routes.ts
import ProductController  from "./ProductController"

const app = express();
const productController = new ProductController();
app.get("/product", productController.getAll);

The tests

// routes.test.ts

import request from "supertest";
import { Request, Response } from "express";

const mockGetAll = jest
  .fn()
  .mockImplementation((req: Request, res: Response) => {
    res.send({ value: "Hello visitor from the future" });
  });

jest.doMock("./ProductController", () => {
  return jest.fn().mockImplementation(() => {
    return {
      getAll: mockGetAll,

    };
  });
});

import app from "./routes";

describe("Routes", () => {
  beforeEach(() => {
    mockGetAll.mockImplementation((req: Request, res: Response) => {
      res.send({ value: "You can also change the implementation" });
    });
  });

  it("GET /product integration test", async () => {
    const result = await request(app).get("/product");
    
    expect(mockGetAll).toHaveBeenCalledTimes(1);

  });



  it("GET an undefined route should return status 404", async () => {
    const response = await request(app).get("/random");
    expect(response.status).toBe(404);
  });
});


I had some issues to get the mocking to work. but using jest.doMock and the specific order you see in the example makes it work.

Solution 9 - node.js

If you want to avoid supertest, you can simply mock the request and Response and test it just like any other async function.

  let handlerStatus = 0;
  let handlerResponse: any = {}; // can replace any with the strong type

  const req: Request = {
    //  inject here the request details
    headers: { authorization: 'XXXXX' },
  } as Request;

  const res = {
    json(body: any) { // can replace any with the strong type
      handlerResponse = body;
    },
    status(status: number) {
      handlerStatus = status;
      return this;
    },
  } ;

  await callYourHanlderFunction(req, res as Response);

  expect(handlerStatus).toBe(200);
  expect(handlerResponse).toEqual(correctResponse);

Attributions

All content for this solution is sourced from the original question on Stackoverflow.

The content on this page is licensed under the Attribution-ShareAlike 4.0 International (CC BY-SA 4.0) license.

Content TypeOriginal AuthorOriginal Content on Stackoverflow
QuestionJamesEggersView Question on Stackoverflow
Solution 1 - node.jsRich ApodacaView Answer on Stackoverflow
Solution 2 - node.jsLuke HView Answer on Stackoverflow
Solution 3 - node.jsLinus ThielView Answer on Stackoverflow
Solution 4 - node.jsRaynosView Answer on Stackoverflow
Solution 5 - node.jsErichBSchulzView Answer on Stackoverflow
Solution 6 - node.jsfxlemireView Answer on Stackoverflow
Solution 7 - node.jsMarcus RådellView Answer on Stackoverflow
Solution 8 - node.jsAlvaroView Answer on Stackoverflow
Solution 9 - node.jsMego ElhawaryView Answer on Stackoverflow