115

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()".

JamesEggers
  • 12,885
  • 14
  • 59
  • 86

9 Answers9

55

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.

Rich Apodaca
  • 28,316
  • 16
  • 103
  • 129
  • 1
    I like this, but is there a way of asserting the viewName (as in the original question) - or would we have to assert on the content of the response? – Alex Dec 02 '13 at 23:51
  • 32
    I agree with your downside, this isn't unit testing. This relies on the integration of all your units to test your application's urls. – Luke H Aug 03 '14 at 15:55
  • 12
    I think it's legal to say that a "route" is really an `integration`, and perhaps testing routes should be left to integration tests. I mean, the functionality of routes matching up to their defined callbacks is presumably already tested by express.js; any internal logic for getting the final result of a route, should ideally be modularized outside it, and those modules should be unit tested. Their interaction, i.e., the route, should be integration tested. Would you agree? – Aditya M P Mar 06 '17 at 12:33
  • @kgpdeveloper E2E and integration test are not the same thing. This is definitely NOT an end to end test. – AndreFeijo May 24 '22 at 06:33
  • @AndreFeijo Supertest is well-known for end-to-end testing. Then request can and normally SHOULD include the HOST so in this example, it is an end-to-end test. It is not different from Cypress. Was anything mocked in this example? Ask yourself that. – kgpdeveloper May 25 '22 at 08:08
  • @AndreFeijo If nock was used, I would classify it integration testing. But it's not FYI. – kgpdeveloper May 25 '22 at 08:33
40

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);
    });
});
Adowrath
  • 701
  • 11
  • 24
Luke H
  • 3,125
  • 27
  • 31
  • 7
    In my opinion, this is the best pattern to use. Many web frameworks across languages use the controller pattern to separate business logic from the actual http response formation functionality. In this way, you can just test the logic and not the whole http response process, which is something that the developers of the framework should be testing on their own. Other things that could be tested in this pattern are simple middlewares, some validation functions, and other business services. DB connectivity testing is a whole different type of testing though – OzzyTheGiant Jun 13 '19 at 20:11
  • 3
    Indeed, a lot of the answers here are really to do with integration/functional testing. – Luke H Jun 27 '19 at 03:59
  • 1
    This is the proper answer. You should focus on testing your logic, not Express. – esmiralha Sep 01 '20 at 12:01
  • 1
    I really like this approach! When I can, I use the OpenAPI generator (https://github.com/OpenAPITools/openapi-generator), which creates an application skeleton from an OpenAPI document. The output of the generator is a route, a controller, and a service. The route needs no testing, controller is minimal and usually should be left alone, and the real business-logic is in the service - where proper code and testing is written. – Yishai Landau Apr 20 '21 at 08:36
22

Change your response object:

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

And it will work.

Linus Thiel
  • 38,647
  • 9
  • 109
  • 104
20

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()
            }
        })
    })
})
Raynos
  • 166,823
  • 56
  • 351
  • 396
9

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;
  });
zbr
  • 6,860
  • 4
  • 31
  • 43
ErichBSchulz
  • 15,047
  • 5
  • 57
  • 61
3

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.

fxlemire
  • 874
  • 9
  • 21
3

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.

Alvaro
  • 107
  • 1
  • 7
2

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, '/');
});
Marcus Rådell
  • 622
  • 7
  • 12
  • 4
    This looks really interesting. Do you have more examples of the missing pieces to show how this all fits together? – cjbarth Aug 03 '16 at 20:29
0

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);