paint-brush
Mocha+Chai: Writing a reusable Test Suite for an expressjs/mongoose APIby@timbushell
1,391 reads
1,391 reads

Mocha+Chai: Writing a reusable Test Suite for an expressjs/mongoose API

by Tim BushellDecember 22nd, 2020
Read on Terminal Reader
Read this story w/o Javascript
tldt arrow

Too Long; Didn't Read

Mocha+Chai: Writing a reusable Test Suite for an expressjs/mongoose API. This article assumes you have an app running alongside a MongoDB server using the mongoose library. We’re focused on testing it. We create a single module; we can put the repetitive clutter from all our tests into a single place. The result of this article is a reusable test suite that can be used to test your expressjs app. It's not dissimilar with a lot of repetitive and cluttered tests.

Company Mentioned

Mention Thumbnail
featured image - Mocha+Chai: Writing a reusable Test Suite for an expressjs/mongoose API
Tim Bushell HackerNoon profile picture

Here is a workable, reusable way to test an expressjs/mongoose application.

What we’re dealing with

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.

The cluttered approach to testing an express API

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.

A good deal clearer

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.

A Finished Test Suite

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:

  • See how the
    api/database.js
    setup we saw in the intro to this article, really helps us here?
  • The before/after is straightforward: Start the mongoServer; connect to it/Stop it; close it.
  • In
    beforeEach
    we clear the database, putting the clearing code into another function we can “await”.
  • The important bit is the callback
    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.
  • I included a
    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.

Usage

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
itself, having setup the app and mongoose servers for them.

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.
  • In your tests (which require auth) use the function by passing in two parameters (the credentials and (surprise surprise) the callback to run the actual test post-login, using the token provided by the callback.
  • This would work for other types of login as well, not just token based login.

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.

Core, Thanks!