Skip to content

Instantly share code, notes, and snippets.

@oscarryz
Last active February 13, 2020 23:23
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save oscarryz/54b8cbc79900895892b9ead3179fec4b to your computer and use it in GitHub Desktop.
Save oscarryz/54b8cbc79900895892b9ead3179fec4b to your computer and use it in GitHub Desktop.
Testing with supertest and mockery

TL;DR

  • Use mockery to inject a mock the require dependency
  • Use supertest to make an http request to the server
  • Use mocha as test runner

How to test express controllers

Setup

Source code for this example

This section will define a simple express app with the following structure:

example
 |
 +- src/
 |   |
 |   + app.js
 |   + homeControler.js
 |   + users.js
 |
 +- test/
 |    |
 |    + app.js
 +- package.json
 +- index.js

There's nothing special about this app, but will illustrate what we are going to test.

You can skip to adding the test

Say we have a controller that uses a library (internal or 3rd party) to fetch data

// homeController.js
const users = require('./users')

// if there are users, display the name of the first one
// If there's none, respond with 404
module.exports = async (req, res) => {
    const data = await users.fetch()
    if (data.length > 0) {
        res.send(data[0].name)
    } else {
        res.sendStatus(404)
    }
}

And the ./users module is defined as follows:

// users.js
const superagent = require('superagent')

module.exports = {
    fetch: async () => {
        console.log('Making a http call'); // for debugging
        const result = await superagent.get('http://jsonplaceholder.typicode.com/users')
        return result.body
    }
}

It defines a fetch function that makes a http request to http://jsonplaceholder.typicode.com/users using superagent library (it could be any http request library)

This controller is usually setup in the app along with other controllers and configurations, typically would look like this:

// app.js
const express = require('express')
const homeController = require('./homeController')

const app = express()

app.get('/', homeController)

module.exports = app

Finally everything is called by an executable file (bin/www, or server.js or something along those lines)

In this example it's just index.js

// index.js
const app = require('./src/app')

app.listen(3000, () => console.log('Started at port 3000'))

Adding tests

Now we want to add a test to check our controller logic.

Note: There are valid arguments saying we should put this logic in separate file module and add methods to it to test it, but this would fail to test the http request parameters and other types of data that comes with and http call (headers, cookies, content type, rendering); the kind of logic that belongs to a controller.

To exercise this logic we can use supertest which starts your actual server and make requests to it (it can also take a Server instance as parameter)

Using mocha as test runner we have our first version like this:

// test/app.js
const supertest = require('supertest')

describe('/', function () {
    it('Should 200 OK when there are users', function (done) {
        supertest(require('../src/app'))
            .get('/')
            .expect(200)
            .end(done)
    })

})

Output:

  /
Making a http call
    ✓ Should 200 OK when there are users (216ms)


  1 passing (222ms)

Test passes and we can see the output includes this message: "Making a http call", we included line in the users.js file for debugging.

// users.js
...
    fetch: async () => {
        console.log('Making a http call'); //<--- here...
        const result = await superagent.get('http://jsonplaceholder.typicode.com/users')
        return result.body
...

While this is great for integration testing it will be very hard to use to test other conditions or add more robust checks. i.e. if the external service is down the app will fail, or add a test to check how our app behaves when the external service is down would be difficult.

To solve this we can use mockery to inject a mock when a require is used in our code.

It's important to keep in mind while using mockery that:

  1. We register the mock before loading the tested library (our app in this case)
  2. We use the same string used by require invocation (just copy/paste it) in the tested code (the controller in our case)
  3. We clean the mock cache after testing to avoid using previous tests caches

We want to mock the users.js module that is used by the controller, in a real application this module would have its own tests of course. So we create a mock object containing the fetch method which is used by the controller.

Updated test source using mockery

//test/app.js
const supertest = require('supertest')
const mockery = require('mockery') // adds mockery 


// Before we start all the tests, enable it. Check mockery documentation to learn more about these flags
before(function () {
    mockery.enable({
        warnOnReplace: false,
        warnOnUnregistered: false,
        useCleanCache: true
    });
});


describe('/', function () {
    it('Should 200 OK when there are users', function (done) {
       
    
        mockery.registerMock(                 // #1 register before use
        './users',                            // #2 Use './users' which **exactly** the same string the controller uses in its require call
        {   
            fetch: () => [{name: 'Mr Test'}] // The mock method that will be invoked
        })
        supertest(require('../src/app'))
            .get('/')
            .expect(200)
            .end(done)
    })
})

// #3 Reset the cache after each test is run. 
// At this point this is not strictly needed but we'll see why it's important
afterEach(function () {
    mockery.resetCache()
})

Output:

  /
    ✓ Should 200 OK when there are users (116ms)


  1 passing (121ms)

Now the "Making a http call" message is gone, because we are actually invoking the mock.

Furthermore we can now add more asserts given the returned data no longer depends on the external service.

        supertest(require('../src/app'))
            .get('/')
            .expect(200)
            .expect('Mr Test') //<-- We can now test the output too, as it's no longer dependent on the external service.
            .end(done)

We can verify we are actually calling the mock by disabling it and see what happens.

  /
Making a http call
    1) Should 200 OK when there are users


  0 passing (316ms)
  1 failing

  1) /
       Should 200 OK when there are users:
     Error: expected 'Mr Test' response body, got 'Leanne Graham'
      at error (node_modules/supertest/lib/test.js:301:13)
      at Test._assertBody (node_modules/supertest/lib/test.js:218:14)
      at Test._assertFunction (node_modules/supertest/lib/test.js:283:11)
      at Test.assert (node_modules/supertest/lib/test.js:173:18)
      at Server.localAssert (node_modules/supertest/lib/test.js:131:12)
      at emitCloseNT (net.js:1618:8)
      at process._tickCallback (internal/process/next_tick.js:63:19)

It's failing of course because it's validating against the real service.

Adding a second test

Now this is the part where using mockery comes handy as it allows us to use a different mock in the same require call. In our case we want to test the part where there are no results.

//..second test
    it('Should 404 Not found when there are no users', function (done) {
        mockery.registerMock('./users', {
            fetch: () => [] // <-- test when there are no results
        })
        supertest(require('../src/app'))
            .get('/')
            .expect(404)
            .end(done)
    })

Output:

  /
    ✓ Should 200 OK when there are users (121ms)
    ✓ Should 404 Not found when there are no users (42ms)


  2 passing (168ms)

The whole new test would look like this now:

const supertest = require('supertest')
const mockery = require('mockery')


before(function () {
    mockery.enable({
        warnOnReplace: false,
        warnOnUnregistered: false,
        useCleanCache: true
    });
});

after(function () {
    mockery.disable();
});
afterEach(function () {
    mockery.resetCache()
})

describe('/', function () {
    it('Should 200 OK when there are users', function (done) {
        mockery.registerMock('./users', {
            fetch: () => [{name: 'Mr Test'}]
        })
        supertest(require('../src/app'))
            .get('/')
            .expect(200)
            .expect('Mr Test')
            .end(done)
    })
    it('Should 404 Not found when there are no users', function (done) {
        mockery.registerMock('./users', {
            fetch: () => []
        })
        supertest(require('../src/app'))
            .get('/')
            .expect(404)
            .end(done)
    })
})

An extra benefit we have using this approach is that we can add test to an existing code base (ymmv) where the code has at least some kind of modularization already without having to refactor the whole project structure to expose internal functionality to be able to test it.

Also, not shown here but supertest can include query parameters, content types, submit files and many other features, check their documentation for more on this

Conclusion

Testing the controller as we expect it to be used creates more reliable code as we don't have to stripdown the logic from the http controller to test it. Using supertest + mockery enables us to create these kinds of tests.

Bear in mind that only the registered mocks will be replaced, any other source (like reading initalization properties, or connecting to other services) will still be invoked, here's where you want to really modularize your app before having to mock everything around it.

Bonus

The module to be stubbed can also be an ES6 class, which is very useful if our code creates a new instance using the new keyword. This was not evident for me at first and couldn't find many examples, but it is really simple.

A class version would be as follows:

// Users.js
class Users {
   constructor(props) {
       // important initialization here
   }
   fetch() {
      //logic
   }
}

Used as:

// controller.js
...
const Users = require('./Users.js')
...
const something = new Users()
something.fetch()

Can be tested as follows:

// Users.js
  it('should...etcetc...
      // class mock
     class Users {
        constructor(props) {
           // now it's empty 
        }
        fetch() {
           return []
        }
     }
    mockery.registerMock('./Users.js', Users); // use it the same way
    supertest(.... etc etc

It was not immediately obvious but now that I think about it makes sense, a class behaves no different than a module in this situation with the extra benefit we can mock the constructor too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment