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() {
});
js to test the actual routes. import request from 'supertest'; import express from 'express'; import router from '../routes/main. js'; const app = new express(); app. use('/', router); describe('Good Home Routes', function () { test('responds to /', async () => { const res = await request(app).
Testing routes can be done both via application tests or container tests. Application tests will likely provide better coverage for routes because routes are typically used to perform transitions and load data, both of which are tested more easily in full context rather than isolation.
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.
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.
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.
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();
});
});
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With