Mock Unit Tests

Linh Huynh
Inside Business Insider
5 min readJun 29, 2020

A core mantra at Insider Inc. is that we should always strive to do better — each day. With that in mind, since joining the company, I have made it a priority to question myself and my assumptions — as well as those made by my team. I constantly ask why we do what we do and how we might challenge ourselves to improve our game.

Through this process, one of the best experiences I’ve had is learning how to do mock unit tests.

In the past few months my team has been working on services that handle data sync with webhook calls and API requests for internal and 3rd party services. In this post, I’ll share a few mock unit tests and show how they work.

What is Mock?

A mock is a method/object that simulates a real method/object behavior for tests. With mocks, we can control the way dependencies — methods that call other external services or methods within them — behave.

Why Mock?

  • Our services manage data sync from API routes and 3rd party services through webhooks. We also use our services to interact with AWS by producing SNS topics. SNS is a simple pub/sub service that groups together messages. If a new message gets published to a topic, SNS will notify its subscribers. That way we can get updates if anything goes wrong with our services. In our case, we use SQS queue as a subscriber. SQS stores the events for the asynchronous process which can be another Lambda function or other service.
  • Part of the reason we use mocks is that we want to test the behaviors of our methods that interact with dependencies, and test their functionality without connecting to any external systems. By mocking we can verify the code that we want to test without worrying about the dependencies’ behaviors.
  • Another concern is that we don’t want to overload our SQS because it may affect how our queues consume messages.
  • Finally, we can also avoid storing any test data in other services.

How to mock with NodeJS?

  • If you do a lot of mock tests you should be familiar with proxyquire. Proxyquire is a 3rd party library which is easy to use and it enables mocking directly Javascript module dependencies.
  • As I mentioned, we handle some methods that produce SNS topic message events and they look like this:
const { aws } = require(‘../../services’)
module.exports = async () => {

// publish SNS messages
await aws.sns.publish(topic, payload, messageAttributes)
}
}

In our mock tests, we set up two cases, success and failure, for that method:

// handle success SNS published message
const SNS_PUBLISH_MOCK = () => {
return new Promise(resolve => resolve({
ResponseMetadata: { RequestId: ‘your-request-id’ },
MessageId: ‘your-message-id’
}))
}
// handle failure SNS published message
const SNS_ERROR_MOCK = () => {
return new Promise((_, reject) => reject(new Error(‘Could not publish sns message’))))
}

Using proxyquire, we can mock the method easily:

const proxyquire = require(‘proxyquire’)
const handler = proxyquire(path, {
‘../../services’: {
aws: {
sns: { publish: SNS_PUBLISH_MOCK/SNS_ERROR_MOCK }
}
}
}
  • Since we use Sequelize, a powerful library that makes it easy to simulate SQL database behaviors, we set up sequelize-mock to mock objects that rely on Sequelize Models. Once Mock models are defined, we can use them as replacements for our Sequelize model objects. Basically, it is just a mock of the Sequelize APIs.
  • For example, let’s say we need to find an existing User in our database:
const { User } = require(‘../models’)
module.exports = async () => {
...
await User.findOne({ where: { email } })
}

To mock the User table, we handle two cases, “User found” and “User not found”:

const SequelizeMock = require(‘sequelize-mock’)
const DBConnectionMock = new SequelizeMock()
// User found
// We can define the User model as default
const USER_MOCK = DBConnectionMock.define(‘User’)
// and set it in proxyquire
const handler = proxyquire(path, {
‘../../models’: { User: USER_MOCK }
}
// User not found: we mock the query by using queryInterface
USER_MOCK.$queryInterface.$useHandler(function (query) {
if (query === ‘findOne’) {
return new Promise(resolve => resolve(undefined))
}
})
// For instance method, sequelize-mock has instanceMethods handlers
USER_MOCK = DBConnectionMock.define(‘User’, {}, {
instanceMethods: {
save: () => new Promise((_, reject) => reject(new Error(‘Error updating user)))
}
})

Besides handling promise mocks, we also take care of multi-level functions such as:

const crypto = require(‘crypto’)
module.exports = async () => {
...
crypto.createHmac(‘sha256’, secretKet).update(buffer).digest(‘base64’)
}
// in test
const crypto = {
createHmac: () => {
return {
update: () => {
return {
digest: () => ‘xxx’
}
}
}
}
}
const handler = proxyquire(path, { crypto })

Coverage

  • Aiming for high coverage is not mandatory but we want to capture all of the edge cases in our tests. Since we use mock tests, we’ve been improving our code coverage well.

Benefits and Conclusion

  • We also do Test-driven development (TDD) which involves coding, testing and refactoring. Developers write tests first and expect tests to fail. Then they write enough code to pass the tests. After all of the code passes the tests, developers refactor the code to improve code quality.
  • Mocking itself prevents you from needing to rely on 3rd party dependencies, but TDD overall has a greater scope.
Ilustration by Gabrien — Insider Inc.
Ilustration by Gabrien Symons
  • Writing unit tests can reduce bugs and increase the stability of projects. The more edge cases we cover in our tests, the less bugs we will expect in the long term.
  • Tests also help us gain more confidence that we are not breaking the existing functionalities in our code base(s) because of any recent changes or refactoring of code. If someone modifies the project and makes a mistake, there is a chance for that to get noticed and to handle the situation through running tests.
  • Last but not least, writing tests is a way to double-check that everything is working correctly. It’s not a chore!

--

--