Express middleware testing made easy.
λ npm install express-unit
Express Unit exports a helper function for running a single middleware. To use, just import
it and pass it a setup
function, your middleware
, and an optional callback
.
import run from 'express-unit'
run(setup|null, middleware[, callback])
setup
- A function defined to set up the Request/Response lifecycle before it entersmiddleware
. Passnull
if not needed.setup
is called with three arguments:req
- A dummyRequest
object.res
- A dummyResponse
object.next
- A function used to proceed tomiddleware
.
middleware
- The middleware function under test. Passed different arguments depending on whether it is an "error handling" middleware.err
- forwarded fromnext(err)
insetup
if the middleware is an error handler.req
- The dummyRequest
object visited bysetup
.res
- The dummyResponse
object visited bysetup
.next
- A function used to signal the completion of themiddlware
.
callback
- An optional function used to inspect the outcome of passing the Request/Response lifecycle throughsetup
andmiddleware
.err
- Forwarded fromnext(err)
inmiddleware
(if any).req
- The dummyRequest
object visited bysetup
andmiddleware
.res
- The dummyResponse
object visited bysetup
andmiddleware
.
import run from 'express-unit'
import { expect, spy } from './test-utils'
import myMiddleware from './my-middleware'
describe('myMiddleware', () => {
it('gets called!', () => {
const setup = spy((req, res, next) => next())
const middleware = spy(myMiddleware)
run(setup, middleware)
expect(setup).to.have.been.calledBefore(middleware)
})
})
Your setup function will be called with a req
, res
, and next
to prepare the request lifecycle for your middleware. This is your opportunity to set headers on req
or spy/stub any relevant methods on res
. Call next
to execute your middleware.
If for some reason you don't want to supply a setup, just pass null
.
run(null, middleware[, callback])
// middleware.js
export default function middleware(req, res, next) {
const token = req.get('x-access-token')
if (token) return next()
const err = new Error('where is your token?')
next(err)
}
// middleware.test.js
describe('middleware', () => {
describe('when the request has a token header', () => {
const setup = (req, res, next) => {
req.headers['x-access-token'] = 'myToken'
next()
}
it('calls next without error', done => {
run(setup, middleware, done)
})
})
})
Express Unit supports callbacks. Pass a callback as the third argument to inspect the results.
// middleware.js
export default function middleware(req, res, next) {
const token = req.get('x-access-token')
if (token) return next()
const err = new Error('Access token required.')
return next(err)
}
// middleware.test.js
describe('middleware', () => {
describe('when the request does not have a token header', () => {
it('passes an Error', done => {
run(null, middleware, (err, req, res) => {
expect(err)
.to.be.an('error')
.with.property('message', 'Access token required.')
done()
})
})
})
})
Express Unit also supports async
middleware. This is any middleware that is an async
function or simply returns a Promise
. express-unit
will resolve an array of [err, req, res]
that you can either await
or receive in a call to then
. Works great with express-async-wrap
.
// middleware.js
import wrap from 'express-async-wrap'
export const middleware = users => wrap(async ({ params }, res, next) => {
const { userId } = params
const user = await users.findById(userId)
if (!user) throw new NotFound(`User ${userId} does not exist.`)
res.locals.user = user
next()
})
// middleware.test.js
describe('middleware', () => {
const user = { id: 1, name: 'foo' }
afterEach(() => {
users.findById.restore()
})
describe('when the user is found', () => {
const setup = (req, res, next) => {
req.params.userId = 1
stub(users, 'findById').resolves(user)
next()
}
it('sets the user on locals', async () => {
const [ err, , res] = await run(setup, middleware(users))
expect(err).to.be.null
expect(users.findById).to.have.been.calledWith(user.id)
expect(res.locals).to.have.property('user', user)
})
})
})
Express Unit puts the "unit" back in unit testing for Express.js apps. It's a small, simple helper for exercising individual middleware functions in isolation. Most testing tutorials and examples for express
apps will utilize supertest
(or similar).
import request from 'supertest'
import app from '../app'
describe('app', () => {
describe('/hello-test', () => {
it('handles GET requests', done => {
request(app)
.get('/hello-test')
.set('Accept', 'application/json')
.expect(200, (err, res) => {
/* make more assertions */
done()
})
})
})
})
This is great for testing your entire app
or a given router
within your app
. For some endpoints, various middleware functions are put in place to determine the response, e.g. confirming a user's identity, verifying their access rights, and bailing out of a request early if preconditions are not met. But testing all of this in concert is integration testing, which is often best left at testing the "happy path". A single route could employ a middleware stack like this:
router
.use(authorize)
.route('/users/:userId/permissions')
.put(userIs('admin'), updatePermissions(permissions))
router
.use(errorHandler(logger))
There are 4 different middleware functions involved in this single route. At least one of which needs access to some kind of data store. But each middleware is very focused and can be reused or replaced (Yay!).