Here is a workable, reusable way to test an expressjs/mongoose application.
It assumes the following setup; i.e. what every expressjs project could be expected to have:
project
|- api
|- app.js
|- database.js
|- test
| test1.test.js
| test2.test.js
| ...
Stripped down, your app will probably look something like this:
app.js
const bodyParser = require("body-parser")
const cors = require("cors")
const express = require("express")
const database = require("./database")
let app = express()
// Pretty standard API app
app
.use(cors())
.use(bodyParser.json())
.use(bodyParser.urlencoded({ extended: true }))
// The article covers this later
database
.dbconnect()
.on("error", err => console.log("Connection to database failed."))
// Standard route
app.get("/", async (req, res) => {
res.send("Hello world")
})
// other... better stuff here!
app.listen(5000, () =>
console.log("Server started at port http://localhost:5000")
)
module.exports = app
Yep! Vanilla expressjs!
The bit to focus on is the line
const database = require(“./database”)
- an import which utilises a nice pattern to handle the Mongo connection implementation. Looking at the module imported will make sense of the database.dbconnect()
call above:const mongoose = require("mongoose")
const { MONGODB_URL } = process.env // or some other source of the cnn str
function dbconnect() {
mongoose.connect(MONGODB_URL, {
useNewUrlParser: true,
useCreateIndex: true,
useUnifiedTopology: true,
useFindAndModify: false,
})
return mongoose.connection
}
function dbclose() {
return mongoose.disconnect()
}
module.exports = { dbconnect, dbclose }
So that’s where we are. Nothing special… a simple expressjs app running alongside a MongoDB server using the mongoose library.
This article assumes you have an app already; we’re focused on testing it.
Let’s imagine how things might look in your tests right now.
test1.test.js
could look something like this:describe("testing POST /wizard", () => {
before(async () => {
//before stuff like setting up the app and mongoose server.
})
beforeEach(async () => {
//beforeEach stuff clearing out the db.
})
after(async () => {
//after stuff like shutting down the app and mongoose server.
})
it("tests wizard creation", done => {
request(app)
.post("/wizard")
.send({ name: "wizard1" })
.end((err, res) => {
res.should.be.tested.thoroughly.here
done()
})
})
})
…
test2.test.js
not dissimilar, with a lot of repetitive garbage:describe("testing POST /apprentice", () => {
before(async () => {
//before stuff like setting up the app and mongoose server.
})
beforeEach(async () => {
//beforeEach stuff clearing out the db.
})
after(async () => {
//after stuff like shutting down the app and mongoose server.
})
it("tests apprentice creation", done => {
chai
.request(app)
.post("/apprentice")
.send({ name: "apprentice1" })
.end((err, res) => {
res.should.be.tested.thoroughly.here
done()
})
})
})
Repetitive and cluttered: If you agree, the purpose of this article is to look at a way of cleaning that all up.
First, we create a module; a single place we can put the repetitive clutter from all our tests. I will call this a “Test Suite”. To separate it visually from the actual tests, I would put it inside a
suites
folder, like so:project
|- api
|- app.js
|- database.js
|- tests
|- test1.test.js
|- test2.test.js
|- suites
|- mochaTestSuite.js
Here is the scaffold of such a suite:
module.exports = (testDescription, testsCallBack) => {
describe(testDescription, () => {
before(async () => {
//before stuff like setting up the app and mongoose server.
})
beforeEach(async () => {
//beforeEach stuff clearing out the db.
})
after(async () => {
//after stuff like shutting down the app and mongoose server.
})
testsCallBack()
})
}
The more eagle-eyed among you will have noticed it isn't much different from the two tests we already have. It has two parameters. A description and a callback.
And this is how we intend to use it in, for example,
test1.test.js
:// usual require stuff we will cover later
const mochaTestSuite = require("./suites/mochaTestSuite.js")
mochaTestSuite("the test description", () => {
it("tests wizard creation", done => {
chai
.request(app)
.post("/wizard")
.send({ name: "wizard1" })
.end((err, res) => {
res.should.be.tested.thoroughly.here
done()
})
})
})
The same eagle-eyes will also have noticed we haven't done anything much more than replace the
describe
function (which was originally in test1 and test2) with the new mochaTestSuite
call — then delete all those before/after calls.In fact, the two parameters - description and the callback for the tests - work in exactly the same way (in the test suite we’re writing) as they do in the bog standard
describe
function we normally use.Rather than laboriously “then add this” and “then do this”… here is, without further preamble, a finished
mochaTestSuite
.// test/suites/mochaTestSuite.js
const chai = require("chai")
const chaiHttp = require("chai-http")
const mongoose = require("mongoose")
const request = require("supertest")
const { MongoMemoryServer } = require("mongodb-memory-server")
const app = require("../../api/app.js")
const mongooseConnect = require("../../api/database")
const should = chai.should()
chai.use(chaiHttp)
module.exports = (testDescription, testsCallBack) => {
describe(testDescription, () => {
// Bonus utility
const signUpThenLogIn = (credentials, testCallBack) => {
chai
.request(app)
.post("/auth/Thing/signup")
.send({
name: "Wizard",
...credentials,
})
.set("Content-Type", "application/json")
.set("Accept", "application/json")
.end((err, res) => {
chai
.request(app)
.post("/auth/Thing/login")
.send(credentials)
.set("Content-Type", "application/json")
.set("Accept", "application/json")
.end((err, res) => {
should.not.exist(err)
res.should.have.status(200)
res.body.token.should.include("Bearer ")
testCallBack(res.body.token)
})
})
}
// Database connector
const clearDB = () => {
for (var i in mongoose.connection.collections) {
mongoose.connection.collections[i].deleteMany(() => {})
}
}
before(async () => {
let mongoServer = new MongoMemoryServer()
const mongoUri = await mongoServer.getUri()
process.env.MONGODB_URL = mongoUri
await mongooseConnect.dbconnect()
})
beforeEach(async () => {
await clearDB()
})
after(async () => {
await clearDB()
await mongooseConnect.dbclose()
})
// Run the tests inside this module.
testsCallBack(signUpThenLogIn)
})
}
A little unpacking:
api/database.js
setup we saw in the intro to this article, really helps us here?beforeEach
we clear the database, putting the clearing code into another function we can “await”.testsCallBack
. What happens is this. When you call mochaTestSuite
, your it
statements are wrapped into the callback function parameter we discussed earlier. mochaTestSuite
first sets up the tests inside its own describe
statement, running the necessary before and after statements, then it (duh) callsback the function you declared with one or more it
functions.signUpThenLogIn
function, which I will explain shortly. The point here is to show how all sorts of reusable code can be put into the suite, decluttering your actual tests.Now we can declutter our tests like:
const chai = require("chai")
const chaiHttp = require("chai-http")
const request = require("supertest")
const mongoose = require("mongoose")
const app = require("../api/app.js")
const mochaSuite = require("./suites/mochaSuite")
const should = chai.should()
chai.use(chaiHttp)
mochaTestSuite("testing POST /wizard", signUpThenLogIn => {
it("creates Wizards", done => {
chai
.request(app)
.post("/wizard")
.send({ name: "wizard1" })
.end((err, res) => {
res.body.name.should.equal("wizard1")
res.should.be.thoroughly.tested.in.other.ways
done()
})
})
})
Sweet. With all the before and after setup being handled by the suite, we can focus on testing functionality.
The
mochaTestSuite
has two parameters, the description and the callback function - which will (duh!) callback to run the actual tests inside And that’s it really.
But I promised to show you how the
signUpAndLogInfunction
would work. Some of your requests may need to be authenticated and it helps to outsource that clutter as well.In fact, it works in a similar way, except
signUpAndLoginIn
’s callback parameter will be inside one of your it tests.// requires as normal
const mochaSuite = require("./suites/mochaSuite")
mochaTestSuite("testing POST /apprentice", signUpThenLogIn => {
it("Logs in Wizard can create Apprentices", done => {
signUpThenLogIn(
{ username: "grandwizard", password: "IShallPass" },
token => {
chai
.request(app)
.post("/apprentice")
.send({ name: "apprentice1" })
// use the token
.set("Authorization", token)
.end((err, res) => {
res.body.name.should.equal("apprentice1")
res.should.be.thoroughly.tested.in.other.ways
done()
})
}
)
})
})
So…
mochaTestSuite
passes signUpThenLogIn
into the callback.This also shows you how other features can be added to the test suite to
“outsource” the laborious, repetitive code that tests need just to get
them prepped for actually running.
Cleaner tests not only help with debugging but help other developers who might be looking at your tests to understand how to use your code.