cusejuice
cusejuice

Reputation: 10681

How to unit test express Router routes

I'm new to Node and Express and I'm trying to unit test my routes/controllers. I've separated my routes from my controllers. How do I go about testing my routes?

config/express.js

  var app = express();
  // middleware, etc
  var router = require('../app/router')(app);

app/router/index.js

  module.exports = function(app) {
    app.use('/api/books', require('./routes/books'));
  };

app/router/routes/books.js

  var controller = require('../../api/controllers/books');
  var express = require('express');
  var router = express.Router();

  router.get('/', controller.index);

  module.exports = router;

app/api/controllers/books.js

// this is just an example controller
exports.index = function(req, res) {
    return res.status(200).json('ok');
};

app/tests/api/routes/books.test.js

  var chai = require('chai');
  var should = chai.should();
  var sinon = require('sinon');

  describe('BookRoute', function() {

  });

Upvotes: 36

Views: 43360

Answers (6)

Alex
Alex

Reputation: 436

An improvement to Jamie's answer:

  1. Validate that the matched method and path belongs to the same route.

  2. Instead of searching the methods using Object.keys(...).includes() you can match directly your method.

      test.each(routes)("`$method` exists on $path", (route) => {
     expect(
       router.stack
         .some(
           (s) =>
             s.route.path === route.path && s.route.methods[route.method]
         )
     ).toBe(true);
    

    });

Upvotes: 0

Hasan Gokce
Hasan Gokce

Reputation: 711

This answer is an improvement of @jamie:

import router from "../payment";

describe("has routes", () => {
  function findRouteByName(routes: any, path: any) {
    return routes.find(
      (layer: any) => layer.route && layer.route.path === path
    );
  }

  const routes = [
    { path: "/", method: "post" },
    { path: "/presale", method: "post" },
    { path: "/deny", method: "post" },
    { path: "/cancel", method: "post" },
    { path: "/refund", method: "post" },
    { path: "/issuer/info", method: "get" },
    { path: "/latest", method: "get" },
    { path: "/history", method: "get" },
    { path: "/issuer/:transactionId", method: "get" },
    { path: "/:paymentId", method: "post" },
    { path: "/wallet/cancel", method: "post" },
    { path: "/wallet/refund", method: "post" },
    { path: "/corporate/payment-amount", method: "get" },
    { path: "/corporate/history", method: "get" },
  ];

  it.each(routes)("`$method` exists on $path", (route) => {
    const expectedMethod = route.method;
    const singleRouteLayer = findRouteByName(router.stack, route.path);
    const receivedMethods = singleRouteLayer.route.methods;

    // Method control
    expect(Object.keys(receivedMethods).includes(expectedMethod)).toBe(true);

    // Path control
    expect(router.stack.some((s) => s.route.path === route.path)).toBe(true);
  });
});

Upvotes: 0

Jamie
Jamie

Reputation: 4295

If you just want to unit test the route's presence and its method, you can do something like this:

auth.router.js

import { Router } from 'express';

const router = Router();

router.post('/signup', signupValidation, signupUser);
router.post('/login', loginValidation, loginUser);
router.post('/reset', resetValidation, setPasswordReset);

export default router;

auth.router.spec.js

test('has routes', () => {
  const routes = [
    { path: '/signup', method: 'post' },
    { path: '/login', method: 'post' },
    { path: '/reset', method: 'post' },
  ]

it.each(routes)('`$method` exists on $path', (route) => {
  expect(router.stack.some((s) => Object.keys(s.route.methods).includes(route.method))).toBe(true)
  expect(router.stack.some((s) => s.route.path === route.path)).toBe(true)
})

Note: The use of $variables in the example test name will only work with Jest ^27.0.0

Edit: Thanks to Keith Yeh for his suggestion to put this into an each() statement. I have updated the code accordingly & the old code is below:

auth.router.spec.js (OLD)

import router from '../auth.router';

test('has routes', () => {
  const routes = [
    { path: '/signup', method: 'post' },
    { path: '/login', method: 'post' },
    { path: '/reset', method: 'post' }
  ]

  routes.forEach((route) => {
    const match = router.stack.find(
      (s) => s.route.path === route.path && s.route.methods[route.method]
    );
    expect(match).toBeTruthy();
  });
});

Upvotes: 16

Peter Haight
Peter Haight

Reputation: 1924

This is interesting because you've separated out your controllers from your routers. The other StackOverflow article mentioned in the comments is a good way to test your controllers, I think. The thing to keep in mind with unit tests is what are you testing exactly. You shouldn't need to write tests to test the express library because presumably it has its own unit tests. So you just need to test your calls to the library. So for the books route, you just need to test this one line of code:

router.get('/', controller.index);

I looked around to see if there was an obvious way to get a list of routes from the express library, but I didn't see one. You can probably just look at the library itself and check its internals to see if you set a route correctly. Another option though is to mock it up and just check that you are calling it correctly.

This is going to get pretty complicated because you need to mock up a some fundamental parts of Javascript in order to test this one line of code. Here's how I did it:

describe('BookRoute', function() {
  it("should route / to books controller index", function() {
    var controller = require('../../../api/controllers/books');
    var orig_this = this;
    var orig_load = require('module')._load;
    var router = jasmine.createSpyObj('Router', ['get']);
    var express = jasmine.createSpyObj('express', ['Router']);
    express.Router.and.returnValues(router);
    spyOn(require('module'), '_load').and.callFake(function() {
      if (arguments[0] == 'express') {
        return express;
      } else {
        return orig_load.apply(orig_this, arguments);
      }
    });
    require("../../../router/routes/books");
    expect(router.get).toHaveBeenCalledWith('/', controller.index);
  });
});

What's going on here is I used Jasmine's spyOn function to spyOn the _load function in module.js which is what handles all of the require calls. This is so that when we require the books router and it calls require('express') we can return our express SpyObj that we created with jasmine.createSpyObj. Once we have replaced express with our spy object, then we can have it return our Router SpyObj which will let us spy on router.get. Then we can check to make sure it is called with '/' and controller.index.

This could probably be made into some sort of utility if you wanted to use this a lot.

I usually avoid a lot of this thing by using a more object oriented approach and either I'm passing around some object everywhere that I can mock for tests or you could use some kind of dependency injection like Angular uses.

Upvotes: 6

ironchefpython
ironchefpython

Reputation: 3409

Code:

config/express.js

var app = express();
// middleware, etc
var router = require('../app/router')(app);

module.exports = app;

app/tests/api/routes/books.test.js

var chai = require('chai');
var should = chai.should();
var sinon = require('sinon');
var request = require('supertest');
var app = require('config/express');

describe('BookRoute', function() {
    request(app)
        .get('/api/books')
        .expect('Content-Type', /json/)
        .expect('Content-Length', '4')
        .expect(200, "ok")
        .end(function(err, res){
           if (err) throw err;
        });
});

Considerations:

If your server requires an initial state at the beginning of a set of tests (because you're executing calls which mutate server state), you'll need to write a function that will return a freshly configured app and the beginning of each group of tests. There is an NPM library: https://github.com/bahmutov/really-need that will allow you to require a freshly instantiated version of your server.

Upvotes: 10

Patrick Motard
Patrick Motard

Reputation: 2660

I found this blog incredibly insightful when testing my own servers endpoints.

In the blog he addresses:

  • How to use the endpoint testing library supertest.

  • How to programmatically spin up and tear down an express server with your needed routes before and after each endpoint test. (he also explains why you would want to do this).

  • How to avoid a common gotcha, require caching your modules required in your unit tests, leading to unintended consequences.

Hope this helps. Good luck and if you have any further questions let me know.

Upvotes: 2

Related Questions