Make your code cleaner and more maintainable by writing Unit Tests for Express routes using Dependency Injection.
Have you ever tried to unit-test Express’ routes? If you have so and if your tests involved some HTTP mock or if you wrote assertions on the route’s response then you weren’t writing Unit Tests.
Let me tell you what I’ve been dealing with since I started using JavaScript on the backend side. But first, I will present some concepts that, in my opinion, have to be understood before writing any code.
Also, I’ll write this article using Express, but this approach can be followed by having Dependency Injection in the router.
Testable Code
My very first approach to JavaScript-based backends was in the 10th year of my career. I realized that the difference between the old me and the new one was that the new me wanted to write Clean Code (if you don’t know what Clean Code is, then check this wonderful book, by Robert C. Martin).
You can’t write Clean Code without tests. You must write tests if you want to refactor your code without worrying about breaking something. Actually, there are a lot more reasons why you should write tests, but that’s not part of this article.
I will produce, with each release, a quick, sure, and repeatable proof that every element of the code works as it should.
The Programmer’s Oath, by Robert C. Martin.
Isolated Tests
The next step is to understand what “Unit Testing” really means… or should we call it “Isolated Tests”? Please, watch this video by J. B. Rainsberger.
Basically, we want to test small units of code, isolating them from their environment and dependencies.
I don’t want to stress too much what Unit Testing is. I’m sure you’re familiarized with the concept. Otherwise, there are plenty of resources to learn this from. One that I really like is Christopher Okhravi’s YouTube channel, and he has a lot of explanations about this topic.
TIP #1:
The Key to Effective Time Management: Turn the Noise Off!
Too many distractions and you can’t finish watching these videos? Turn the noise off! Learn how to manage your time more efficiently starting today:
Route: method + path + callback
How do you define routes in Express? Let’s analyze Express’ API:
app.METHOD(path, callback [, callback ...])
- METHOD: is the HTTP method of the request, such as GET, PUT, POST, and so on, in lowercase.
- path: The path for which the middleware function is invoked.
- callback: Callback functions.
So, in human words, defining a route is a process where we define a rule: “when someone calls this path using this method you will execute this/these function/s”.
Note that I’m not saying anything about the response, arguments, HTTP status code, authentication, authorization… no, nothing but the rule: method/path → callback/s.
Non-isolated approach (maybe the one that you have been using so far)
Let’s say we have a very simple route GET /people
that returns a list of people.
The approach for testing it, that I’ve read in a lot of places, is the following:
- Start your web server or, better, mockup your web server.
- Call the route.
- Analyze the HTTP status code.
- Analyze the data from the response.
Simple, isn’t it? But it’s wrong. You’re not unit testing it.
If you aren’t using a mock for your web server, then you’re testing everything from the routes to the model (MVC).
You may even be reading data from the database, and making your tests longer and more complex.
Clean code reads like well-written prose.
Clean Code, by Robert C. Martin.
If you’re smart enough to decide to mock up your web server, you’re on a better path. But you are still checking the HTTP status code, aren’t you? you are still checking the response, aren’t you?
But if you just want to test routes, why are you writing those assertions? Isn’t the response of a route the responsibility of the route’s handler (aka “Controller”)?
If you knew that, it’s okay. But don’t lie to yourself, you are not writing Unit Tests, or at least, your tests are more coupled than they should be (they’re not isolated enough).
You are testing the Router and the Controller. Everything lives in a single and long file (or a lot of smaller files…).
A simpler approach — but first, some refactors
Let’s write some simple code for our GET /people
route. Also, let’s assume that we have a People
model with a findAll
method in the file src/models/people.js
.
TIP #2:
Inbox Zero: My 7-step approach
Don’t let your Inbox distract you so that you can complete reading this article 😉 Learn the ninja skills for mastering your inbox!
src/index.js
import express from 'express'
import People from './models/people'
const PORT = 3000
app = express()
app.get('/people', async (req, res) => {
try {
const people = await People.findAll()
res.send(people)
} catch(err) {
res.status(500).send('Something went wrong')
}
})
app.listen(PORT, () => {
console.log(`Server running at https://localhost:${PORT}`)
})
That’s a simple and small file. But it has only one route… what would happen if it had 5 resources (people, cities, cars, …) with their respective routes?
It would be great to split this out into different files. But first, let’s divide the Route from the Controller (or handler function):
src/index.js
import express from 'express'
import People from './models/people'
const PORT = 3000
app = express()
const getPeople = async (req, res) => {
try {
const people = await People.findAll()
res.send(people)
} catch(err) {
res.status(500).send('Something went wrong')
}
}
app.get('/people', getPeople)
app.listen(PORT, () => {
console.log(`Server running at https://localhost:${PORT}`)
})
A smooth step, but an important one. Now the route expresses its intention of executing the getPeople
function when the client executes a GET
request to the /people
path.
Any fool can write code that a computer can understand. Good programmers write code that humans can understand.
Refactoring, by Martin Fowler.
NOTE: If you are following this process with your routes and you have tests for them, you can (and should) run them after every refactor we do.
I’d like to make two more steps before going to the test part:
- I’d like to move the controllers to different files, but maybe one file per controller’s function it’s too much, so I’ll create a PeopleController with all its methods.
- I’m going to move the routes to different files, using the same way. Also, the router will be a function that will define the routes.
src/controllers/people.js
export const getPeople = async (req, res) => {
try {
const people = await People.findAll()
res.send(people)
} catch(err) {
res.status(500).send('Something went wrong')
}
}
src/routes/people.js
import * as PeopleController from '../controllers/people'
const loadPeopleRoutes = (app, controller = PeopleController) => {
app.get('/people', controller.getPeople)
}
export default loadPeopleRoutes
Note that we’re injecting the controller to the router. This will be essential to be able to isolate the router.
And now the src/index.js
will look a lot cleaner:
src/index.js
import express from 'express'
import loadPeopleRoutes from './routes/people'
const PORT = 3000
app = express()
loadPeopleRoutes(app)
app.listen(PORT, () => {
console.log(`Server running at https://localhost:${PORT}`)
})
I’m sure you’ll notice that we could create a general loadRoutes
method that reads all the files inside routes
and load all the routes for our web server.
Writing the tests
After a few refactors, our code is cleaner and more structured. We have the index file to run the web server with all its middlewares, error catching, etc; a directory for routes (one file per resource); a directory for controllers (one file per resource); and a directory for the models (one file per model). Beautiful!
TIP #3:
Going Full (Screen) and the Open-Complete-Close Technique
Use the OCC (Open-Complete-Close) technique when coding so that you can be more organized, agile, and efficient:
I’m not going to show how to set a boilerplate for testing up, you can do it with the framework you like the most. I’m just going to focus on the testing part.
Note that I’m going to use Sinon to mock dependencies.
Mocking is a technique to isolate test subjects by replacing dependencies with objects that you can control and inspect.
Understanding Jest Mocks, by Rick Hanlon II (link).
test/routes/people.js
import sinon from 'sinon'
import loadPeopleRoutes from '../../src/routes/people'
describe('Routes for People', () => {
const app = { get: () => {} }
const controller = { getPeople: () => {} } before('Initialize spies', () => {
sinon.spy(app, 'get')
}) before('Load routes', () => {
loadPeopleRoutes(app, controller)
}) after('Reset spies', () => {
app.get.restore()
}) it('should define the route \'GET /people\' and call to the right handler', () => {
expect(app.get).to.be.calledOnceWithExactly(
'/people',
controller.getPeople
)
})
})
That’s a unit test for a route. You can extend this approach in case your route has more handlers.
What’s next?
I wrote this post just to explain my thinking about how to test routes in an isolated way. You may want to follow the same approach to write isolated tests for the rest of your application.
When you write the tests for your controllers, that’s when you can (and should) do assertions about arguments, HTTP status codes, responses, etc. Because those things are part of the responsibility of the controller.
Another good thing to do here is to inject the models to the controllers. This way, again, you can test controllers in an isolated environment.
I hope I made myself clear enough to express my intention here. I’ve been coding for years now and I learned to hear opinions before giving them. But this topic is something that has bothered me for a long time, so I decided to write a few words about it.
Any comment or suggestion is more than welcome.
Thanks for reading.