Testing in Node.js Using Mocha and Chai [2/2]

Advance use cases for testing your NodeJS APIs

Feature Image

We introduced you to the basics of testing in Node.js in the previous post. As we promised, this post picks up from where the previous post left off and discusses more advanced concepts related to testing a Node.js program.

In this post, we are going to introduce you to the second level of testing software, integration testing, and common testing practices like mocking and stubbing.

To start, let’s understand what integration testing is and why we need it.


What is integration testing?

Integration testing combines several units (that we tested in unit testing) and test them as a single group. It inspects the system for bugs related to the interaction between units.

We can use integration testing to test data retrieval from databases, remote APIs, and API endpoints. The use of integration testing increases the expanse of the code covered by tests.

Usually, integration testing is the responsibility of the developer. But it’s not rare for independent testers to carry out the testing.


Integration testing in Node.js with Mocha and Chai

To start with Node.js integration testing, we will use Mocha and Chai NPM packages. We are going to use an Express server with REST endpoints for our testing purposes. To send HTTP requests to this server during testing, we use a new package called chai-http. Make sure to install chai-http using npm install command before continuing.


Testing asynchronous functions

When testing asynchronous functions using Mocha, we have to follow a slightly different format to the one we used in the previous tutorial. Even though I used the word “new” to describe this format, it is not new for a Node.js developer. We can pass a callback or if the asynchronous function returns a Promise, use Promises or async/await inside the “it” function to deal with the asynchronous code as we normally do in Node.js.

describe("Test asynchronous code", () => {

	//using a callback function
	it("should return the intended output", done => {
    	callToAsyncFunction(input, (result) = {
        	//implement testing logic

        	//call the callback function
        	done()
    	})
	})

	//using promises
	it("should return another output", () => {
    	return callToAsyncFunction(input).then(result => {
        	//implement result testing logic
    	})
	})

	//using async/await
	it("should return a different output", async () => {
    	let result = await callToAsyncFunction(input)
    	//implement testing logic

	})
})

Write test cases for testing your API

For your first implementation of integration testing, we are using a REST API that retrieves relevant data from a database on requests and sends them back to the client. Assume that the database (we are using a MongoDB database) and data models of the application have already been set for the clarity of the implementation of tests.

The POST route /dogs is used to save a new dog to the database using the data sent from the client-side through a POST request. The Dog model we are using in this step has 3 attributes: name, age, and breed of the dog. We need to pass this data with the HTTP request to the /dog route to create a new dog.

const Dog = require("./models/dog")

app.get('/dogs', (req, res) => {
	let {name, age, breed} = req.body

	let newDog = new Dog({
    	name,
    	age,
    	breed
	})

	newDog.save((err) => {
    	if (err){
        	return res.status(500).send(err.message)
    	}
    	res.status(200).send()

	})
})

When all the routes of the API are handled, don’t forget to export the “app” object so that we can access the server for testing.

module.exports = app

Create a file named dogs.js inside the test directory to write the test cases for this route. Inside this file, we are testing the route to see if it responds with the results we expect. In this case, it is a confirmation that the new dog was saved to the database successfully.

However, since we are only testing out this code, we need to leave the database in its initial state after the test case is finished. So, we have to delete every new record entered into the database after every test case. We can use Mocha’s afterEach hook to achieve this.

const chai = require("chai")
const chaiHttp = require("chai-http")
const expect = chai.expect
chai.use(chaiHttp)
const app = require('../app')
const Dog = require("./models/dog")

describe("POST /dogs", () => {

	it("should return status 200", async () => {
    	let res = await chai
        	.request(app)
        	.post('/dogs')
        	.send({name: "Charlie", age: "9", breed: "Pomerian"})
       
    	expect(res.status).to.equal(200)
       
	})

	afterEach(async () => {
    	await Dog.deleteOne({name: "Charlie"})
	})
})

We use async/await to handle the asynchronous route handling function. Then, we use Chai (which is using chai-http) to send a POST request to the server. We can send the body of the POST requests with the request using the send method. Chai’s expect function is used to assert that the response is equal to what we expect.

You can follow the same test process when testing much complex API endpoints. If testing involves entering or retrieving data from a database, make sure to leave the database in its initial state using Mocha’s beforeEach, afterEach functions to enter and delete records.

Using stubs with Sinon

Stubs are used as temporary replacements for functions that are used by components under testing. We use a stub to simulate the behavior of a given function. Reasons for using stubs instead of the real implementation of the function varies depending on the situation. Stubs are especially useful in unit testing when we only want to test the behavior of a single unit. In integration testing, stubs are used when the given function is not yet implemented but it is required to test the current component. When several components are tied to another component going through integration testing, we can use stubs to replace some of these components if we want only want to test how only a few of the components work together.

Assume that the above POST /dogs route has a middleware to check whether the user sending the request is logged in or not. (You can implement the isLoggedIn function appropriately)

const Dog = require("./models/dog")
const {isLoggedIn} = require('./middleware')

app.get('/dogs', isLoggedIn, (req, res) => {
	let {name, age, breed} = req.body

	let newDog = new Dog({
    	name,
    	age,
    	breed
	})

	newDog.save((err) => {
    	if (err){
        	return res.status(500).send(err.message)
    	}
    	res.status(200).send()

	})
})

But at the moment, we only want to test the functionality of saving the dog’s details into the database and sending the response. In other words, we don’t want to test how the route handling function and middleware work together for now. So, we can create a stub to replace the middleware function.

We are using the NPM package named Sinon to create stubs for our Node.js program. You should go ahead and install the package to your application before continuing.

We use Sinon’s callsFake function to create the middleware stub. We create this stub before each test case using the beforeEach hook.

const chai = require("chai")
const chaiHttp = require("chai-http")
const expect = chai.expect
chai.use(chaiHttp)
const Dog = require("./models/dog")
const sinon = require("sinon")
const middleware = require('../middleware')

describe("POST /dogs", () => {

	beforeEach(()=> {
       
    	//replace the isLoggedIn function in the middleware module with this fake function
    	loggedInStub = sinon.stub(middleware, 'isLoggedIn').callsFake((req, res, next) => {next()})
    	app = require('../app')
	})

	it("should return status 200", async () => {
    	let res = await chai
        	.request(app)
        	.post('/dogs')
        	.send({name: "Charlie", age: "9", breed: "Pomerian"})
       
    	expect(res.status).to.equal(200)
       
	})

	afterEach(() => {
    	await Dog.deleteOne({name: "Charlie"})
    	loggedInStub.restore()
	})
})

We need to reimport the app object before each test case, so we place that inside the beforeEach hook as well. After every test case, we need to restore the stub.

Mocking HTTP requests with Nock

If the component under testing has to retrieve data from an external API or a service, it needs to send an HTTP request to this API/service and wait for the response to arrive. If we are not explicitly testing the connectivity to this API, not sending an actual request to the external API reduces the testing time and guarantees that the test does not fail due to reasons like poor network connectivity. If we are not sending an actual request to the API, we need to fake this request during testing and this practice is called mocking HTTP requests.

In this tutorial, we use another NPM module named Nock to mock HTTP requests to external APIs. It intercepts external requests and allows us to return custom responses to suit a particular test case.

Assume that our API has the route GET /dogs/:breed. It returns the sub-breeds of a given dog breed by sending a request to the Dog API . (https://dog.ceo/api/breed//list). Our application server sends a GET request to the Dog API and the data returned from the Dog API is then sent back to the client.

We use the package, Superagent, to send a request to the external API.

const request = require('superagent')

app.get('/dogs/:breed', async (req, res) => {
	let breed = req.params.breed
	let result = await request
    	.get(`https://dog.ceo/api/breed/${breed}/list`)
   
	res.send(result.text)
})

It retrieves the name of the breed from the URL and sends a GET request to the Dog API to get the sub-breeds.

Now, we can test this route for possible bugs. Since we mock the request to the external API and send a custom response during testing, we need to save this custom response in a file to be able to retrieve it whenever we want. I saved it inside a file named “response.js” inside the test directory.

module.exports = {
	message: [
    	"afghan",
    	"basset",
    	"blood",
    	"english",
    	"ibizan",
    	"plott",
    	"walker"
	],
	status: "success"
}

Now, let’s write the test case for the above route. Similar to what we did before, here, we are defining the mock HTTP request to send using Nock inside a beforeEach hook.

describe("GET /dogs/:breed", () => {

	beforeEach(() => {
    	nock('https://dog.ceo')
        	.get('/api/breed/hound/list')
        	.reply(200, response)
	})

	it("should return sub breeds of a dog breed", async () => {
    	let breed = "hound"
    	let res = await chai
        	.request(app)
        	.get(`/dogs/${breed}`)

    	expect(res.status).to.equal(200)
    	expect(res.body.status).to.equal('success')
    	expect(res.body.message).to.have.a.length(7)
	})
})

When we send a GET request to the /dogs/:breed route, nock intercepts the call to the Dog API and returns the custom response we saved in a file. We can change the output of the response appropriately to use for different test cases.


Conclusion

In today’s tutorial, we discussed integration testing and when to apply integration testing, and two of the common but advanced testing practics: stubbing and mocking. With that, our two-part post series on Testing in Node.js concludes. We hope you enjoyed the tutorials and gained at least the basic idea of how to write test cases for your program.

Thanks for reading!

Join the Free Newsletter

A free, weekly e-mail with the best new articles, courses, and special bonuses.

We won't send you spam. Unsubscribe at any time.