Editor’s note: This article was last updated on 5 January 2023 to ensure that all information is compatible with the latest versions of Mocha, Chai, and Sinon at the time of writing.
Tests help document the core features of an application. Properly written tests ensure that new features do not introduce changes that break the application.
An engineer maintaining a codebase might not necessarily be the same engineer that wrote the initial code. If the code is properly tested, another engineer can confidently add new code or modify existing code with the expectation that the new changes will not break other features or, at the very least, will not cause side effects to other features.
JavaScript and Node.js have many testing and assertion libraries, like Jest, Jasmine, Qunit, and Mocha. In this article, we will look at how to use Mocha for testing, Chai for assertions, and Sinon for mocks, spies, and stubs.
Jump ahead:
Unit tests are pieces of code that examine if a function performs as expected when separated from other components of your application. Unit tests allow us to test the different functions in our applications. There are a couple of reasons to consider writing unit tests for your applications:
Mocha is a feature-rich JavaScript test framework that runs on Node.js and in the browser. It encapsulates tests in test suites (describe
block) and test cases (it
block).
Mocha has many interesting features:
before
, after
, beforeEach
, afterEach
Hooks, etc.Having been used for many years, Mocha is an established testing tool. It receives good support and has a sizable user base. Mocha is a little more complicated than some of the other testing tools out there when it comes to ease of use but when used properly, Mocha can be extremely powerful. Because Mocha is a better-developed product with a larger contributor community, it comes with more functionality out of the box.
To check for equality or compare expected results against actual results, we can use the Node.js built-in assertion module. However, when an error occurs, the test cases will still pass. So Mocha recommends using other assertion libraries. For this tutorial, we will be using Chai.
Chai exposes three assertion interfaces: expect()
, assert()
, and should()
. Any of them can be used for assertions.
Often, the method that is being tested is required to interact with or call other external methods. Therefore you need a utility to spy, stub, or mock those external methods. This is exactly what Sinon does for you.
Stubs, mocks, and spies make tests more robust and less prone to breakage should dependent codes evolve or have their internals modified.
A spy is a fake function that keeps track of the following for all that it calls:
this
A stub is a spy with predetermined behavior.
We can use a stub to:
A mock is a fake function (like a spy) with pre-programmed behavior (like a stub) as well as pre-programmed expectations.
We can use a mock to:
The rule of thumb for a mock is: if you are not going to add an assertion for some specific call, don’t mock it. Use a stub instead.
To demonstrate what we have explained above we will be building a simple Node application that creates and retrieves a user. The complete code sample for this article can be found on CodeSandbox.
Let’s create a new project directory for our user app project:
mkdir mocha-unit-test && cd mocha-unit-test mkdir src
Create a package.json
file within the source folder and add the code below:
// src/package.json { "name": "mocha-unit-test", "version": "1.0.0", "description": "", "main": "app.js", "scripts": { "test": "mocha './src/**/*.test.js'", "start": "node src/app.js" }, "keywords": [ "mocha", "chai" ], "author": "Godwin Ekuma", "license": "ISC", "dependencies": { "dotenv": "^6.2.0", "express": "^4.18.2", "jsonwebtoken": "^8.5.1", "morgan": "^1.10.0", "mysql2": "^2.3.3", "pg": "^7.18.2", "pg-hstore": "^2.3.4", "sequelize": "^5.22.5" }, "devDependencies": { "chai": "^4.3.7", "faker": "^4.1.0", "mocha": "^10.2.0", "sinon": "^15.0.1" } }
Run npm install
to install project dependencies.
Notice that test-related packages mocha
, chai
, sinon
, and faker
are saved in the dev-dependencies.
The test
script uses a custom glob (./src/**/*.test.js
) to configure the file path of test files. Mocha will look for test files (files ending with .test.js
) within the directories and subdirectories of the src
folder.
We will structure our application using the controller, service, and, repository pattern so our app will be broken into the repositories, services, and controllers. This pattern breaks up the business layer of the app into three distinct layers:
UserRepository
, you would create methods that write/read a user to and from the databaseUserService
would be responsible for performing the required logic in order to create a new userBreaking down applications this way makes testing easy.
UserRepository
classLet’s begin by creating a repository class:
// src/user/user.repository.js const { UserModel } = require("../database"); class UserRepository { constructor() { this.user = UserModel; this.user.sync({ force: true }); } async create(name, email) { return this.user.create({ name, email }); } async getUser(id) { return this.user.findOne({ id }); } } module.exports = UserRepository;
The UserRepository
class has two methods: create
and getUser
. The create
method adds a new user to the database while getUser
searches a user from the database.
Let’s test the userRepository
methods below:
// src/user/user.repository.test.js const chai = require("chai"); const sinon = require("sinon"); const expect = chai.expect; const faker = require("faker"); const { UserModel } = require("../database"); const UserRepository = require("./user.repository"); describe("UserRepository", function() { const stubValue = { id: faker.random.uuid(), name: faker.name.findName(), email: faker.internet.email(), createdAt: faker.date.past(), updatedAt: faker.date.past() }; describe("create", function() { it("should add a new user to the db", async function() { const stub = sinon.stub(UserModel, "create").returns(stubValue); const userRepository = new UserRepository(); const user = await userRepository.create(stubValue.name, stubValue.email); expect(stub.calledOnce).to.be.true; expect(user.id).to.equal(stubValue.id); expect(user.name).to.equal(stubValue.name); expect(user.email).to.equal(stubValue.email); expect(user.createdAt).to.equal(stubValue.createdAt); expect(user.updatedAt).to.equal(stubValue.updatedAt); }); }); });
The code above is testing the create
method of the UserRepository
. Notice that we are stubbing the UserModel.create
method. The stub is necessary because our goal is to test the repository and not the model. We use faker
for the test fixtures:
// src/user/user.repository.test.js const chai = require("chai"); const sinon = require("sinon"); const expect = chai.expect; const faker = require("faker"); const { UserModel } = require("../database"); const UserRepository = require("./user.repository"); describe("UserRepository", function() { const stubValue = { id: faker.random.uuid(), name: faker.name.findName(), email: faker.internet.email(), createdAt: faker.date.past(), updatedAt: faker.date.past() }; describe("getUser", function() { it("should retrieve a user with specific id", async function() { const stub = sinon.stub(UserModel, "findOne").returns(stubValue); const userRepository = new UserRepository(); const user = await userRepository.getUser(stubValue.id); expect(stub.calledOnce).to.be.true; expect(user.id).to.equal(stubValue.id); expect(user.name).to.equal(stubValue.name); expect(user.email).to.equal(stubValue.email); expect(user.createdAt).to.equal(stubValue.createdAt); expect(user.updatedAt).to.equal(stubValue.updatedAt); }); }); });
To test the getUser
method, we have to also stub UserModel.findone
. We use expect(stub.calledOnce).to.be.true
to assert that the stub is called at least once. The other assertions are checking the value returned by the getUser
method.
UserService
class// src/user/user.service.js const UserRepository = require("./user.repository"); class UserService { constructor(userRepository) { this.userRepository = userRepository; } async create(name, email) { return this.userRepository.create(name, email); } getUser(id) { return this.userRepository.getUser(id); } } module.exports = UserService;
The UserService
class also has two methods create
and getUser
. The create
method calls the create
repository method passing name and email of a new user as arguments. The getUser
calls the repository getUser
method.
Let’s test the userService
methods below:
// src/user/user.service.test.js const chai = require("chai"); const sinon = require("sinon"); const UserRepository = require("./user.repository"); const expect = chai.expect; const faker = require("faker"); const UserService = require("./user.service"); describe("UserService", function() { describe("create", function() { it("should create a new user", async function() { const stubValue = { id: faker.random.uuid(), name: faker.name.findName(), email: faker.internet.email(), createdAt: faker.date.past(), updatedAt: faker.date.past() }; const userRepo = new UserRepository(); const stub = sinon.stub(userRepo, "create").returns(stubValue); const userService = new UserService(userRepo); const user = await userService.create(stubValue.name, stubValue.email); expect(stub.calledOnce).to.be.true; expect(user.id).to.equal(stubValue.id); expect(user.name).to.equal(stubValue.name); expect(user.email).to.equal(stubValue.email); expect(user.createdAt).to.equal(stubValue.createdAt); expect(user.updatedAt).to.equal(stubValue.updatedAt); }); //test a case of no user it("should return an empty object if no user matches the provided id", async function() { const stubValue = {}; const userRepo = new UserRepository(); const stub = sinon.stub(userRepo, "getUser").returns(stubValue); const userService = new UserService(userRepo); const user = await userService.getUser(1); expect(stub.calledOnce).to.be.true; expect(user).to.deep.equal({}) }); }); });
The code above is testing the UserService create
method. We have created a stub for the repository create
method. The code below will test the getUser
service method:
const chai = require("chai"); const sinon = require("sinon"); const UserRepository = require("./user.repository"); const expect = chai.expect; const faker = require("faker"); const UserService = require("./user.service"); describe("UserService", function() { describe("getUser", function() { it("should return a user that matches the provided id", async function() { const stubValue = { id: faker.random.uuid(), name: faker.name.findName(), email: faker.internet.email(), createdAt: faker.date.past(), updatedAt: faker.date.past() }; const userRepo = new UserRepository(); const stub = sinon.stub(userRepo, "getUser").returns(stubValue); const userService = new UserService(userRepo); const user = await userService.getUser(stubValue.id); expect(stub.calledOnce).to.be.true; expect(user.id).to.equal(stubValue.id); expect(user.name).to.equal(stubValue.name); expect(user.email).to.equal(stubValue.email); expect(user.createdAt).to.equal(stubValue.createdAt); expect(user.updatedAt).to.equal(stubValue.updatedAt); }); }); });
Again we are stubbing the UserRepository getUser
method. We also assert that the stub is called at least once and then assert that the return value of the method is correct.
UserController
class/ src/user/user.controller.js class UserController { constructor(userService) { this.userService = userService; } async register(req, res, next) { const { name, email } = req.body; if ( !name || typeof name !== "string" || (!email || typeof email !== "string") ) { return res.status(400).json({ message: "Invalid Params" }); } const user = await this.userService.create(name, email); return res.status(201).json({ data: user }); } async getUser(req, res) { const { id } = req.params; const user = await this.userService.getUser(id); return res.json({ data: user }); } } module.exports = UserController;
The UserController
class has register
and getUser
methods as well. Each of these methods accepts two parameters req
and res
objects:
// src/user/user.controller.test.js describe("UserController", function() { describe("register", function() { let status json, res, userController, userService; beforeEach(() => { status = sinon.stub(); json = sinon.spy(); res = { json, status }; status.returns(res); const userRepo = sinon.spy(); userService = new UserService(userRepo); }); it("should not register a user when name param is not provided", async function() { const req = { body: { email: faker.internet.email() } }; await new UserController().register(req, res); expect(status.calledOnce).to.be.true; expect(status.args\[0\][0]).to.equal(400); expect(json.calledOnce).to.be.true; expect(json.args\[0\][0].message).to.equal("Invalid Params"); }); it("should not register a user when name and email params are not provided", async function() { const req = { body: {} }; await new UserController().register(req, res); expect(status.calledOnce).to.be.true; expect(status.args\[0\][0]).to.equal(400); expect(json.calledOnce).to.be.true; expect(json.args\[0\][0].message).to.equal("Invalid Params"); }); it("should not register a user when email param is not provided", async function() { const req = { body: { name: faker.name.findName() } }; await new UserController().register(req, res); expect(status.calledOnce).to.be.true; expect(status.args\[0\][0]).to.equal(400); expect(json.calledOnce).to.be.true; expect(json.args\[0\][0].message).to.equal("Invalid Params"); }); it("should register a user when email and name params are provided", async function() { const req = { body: { name: faker.name.findName(), email: faker.internet.email() } }; const stubValue = { id: faker.random.uuid(), name: faker.name.findName(), email: faker.internet.email(), createdAt: faker.date.past(), updatedAt: faker.date.past() }; const stub = sinon.stub(userService, "create").returns(stubValue); userController = new UserController(userService); await userController.register(req, res); expect(stub.calledOnce).to.be.true; expect(status.calledOnce).to.be.true; expect(status.args\[0\][0]).to.equal(201); expect(json.calledOnce).to.be.true; expect(json.args\[0\][0].data).to.equal(stubValue); }); }); });
In the first three it
blocks, we are testing that a user will not be created when one or both of the required parameters (email and name) are not provided. Notice that we are stubbing the res.status
and spying on res.json
:
describe("UserController", function() { describe("getUser", function() { let req; let res; let userService; beforeEach(() => { req = { params: { id: faker.random.uuid() } }; res = { json: function() {} }; const userRepo = sinon.spy(); userService = new UserService(userRepo); }); it("should return a user that matches the id param", async function() { const stubValue = { id: req.params.id, name: faker.name.findName(), email: faker.internet.email(), createdAt: faker.date.past(), updatedAt: faker.date.past() }; const mock = sinon.mock(res); mock .expects("json") .once() .withExactArgs({ data: stubValue }); const stub = sinon.stub(userService, "getUser").returns(stubValue); userController = new UserController(userService); const user = await userController.getUser(req, res); expect(stub.calledOnce).to.be.true; mock.verify(); }); }); });
For the getUser
test we mocked on the json
method. Notice that we also had to use a spy in place UserRepository
while creating a new instance of the UserService
.
Run the test using the command below:
npm test
You should see the tests passing:
We have seen how we can use a combination of Mocha, Chai, and Sinon to create a robust test for a Node application. Be sure to check out their respective documentation to broaden your knowledge of these tools. Got a question or comment? Please drop them in the comment section below.
Deploying a Node-based web app or website is the easy part. Making sure your Node instance continues to serve resources to your app is where things get tougher. If you’re interested in ensuring requests to the backend or third-party services are successful, try LogRocket.
LogRocket is like a DVR for web and mobile apps, recording literally everything that happens while a user interacts with your app. Instead of guessing why problems happen, you can aggregate and report on problematic network requests to quickly understand the root cause.
LogRocket instruments your app to record baseline performance timings such as page load time, time to first byte, slow network requests, and also logs Redux, NgRx, and Vuex actions/state. Start monitoring for free.
Hey there, want to help make our blog better?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowBuild scalable admin dashboards with Filament and Laravel using Form Builder, Notifications, and Actions for clean, interactive panels.
Break down the parts of a URL and explore APIs for working with them in JavaScript, parsing them, building query strings, checking their validity, etc.
In this guide, explore lazy loading and error loading as two techniques for fetching data in React apps.
Deno is a popular JavaScript runtime, and it recently launched version 2.0 with several new features, bug fixes, and improvements […]
10 Replies to "Node.js unit testing using Mocha, Chai, and Sinon"
Is there a GitHub repository for your examples ?
GitHub repository: https://github.com/GodwinEkuma/mocha-chai-unit-test
Should the user repository take UserModel as an argument in the constructor? I’m wondering how the test suite for UserRepository knows anything about the stubbed model if the stubbed model is not passed in to the constructor.
The Model exists in the global scope of the user repository, this allows the repository class have access to it. So in the test if the UserModel is referenced the stub will passed instead of the actual model.
Hi thanks for sharing!. I have a question if i wanna return response with 500 status code if repository can not save user to database i.e database connection is broken or like that.
Hey Godwin Ekuma,
How to test case getUser when user does not exist?
I getting this error, please help me to solve this!!!
…\node_modules\yargs\yargs.js:1172
else throw err
^
Error: Cannot find module ‘../database’
Require stack:
– E:\Intenships\vivasvant systems llp\Web development\src\js\mocks\user.repository.test.js
– E:\Intenships\vivasvant systems llp\Web development\node_modules\mocha\lib\mocha.js
– E:\Intenships\vivasvant systems llp\Web development\node_modules\mocha\lib\cli\one-and-dones.js
– E:\Intenships\vivasvant systems llp\Web development\node_modules\mocha\lib\cli\options.js
– E:\Intenships\vivasvant systems llp\Web development\node_modules\mocha\bin\mocha
at Function.Module._resolveFilename (internal/modules/cjs/loader.js:610:15)
at Function.Module._load (internal/modules/cjs/loader.js:526:27)
at Module.require (internal/modules/cjs/loader.js:666:19)
at require (internal/modules/cjs/helpers.js:16:16)
at Object. (E:\Intenships\vivasvant systems llp\Web development\src\js\mocks\user.repository.test.js:5:23)
at Module._compile (internal/modules/cjs/loader.js:759:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:770:10)
at Module.load (internal/modules/cjs/loader.js:628:32)
at Function.Module._load (internal/modules/cjs/loader.js:555:12)
at Module.require (internal/modules/cjs/loader.js:666:19)
at require (internal/modules/cjs/helpers.js:16:16)
at E:\Intenships\vivasvant systems llp\Web development\node_modules\mocha\lib\mocha.js:334:36
at Array.forEach ()
at Mocha.loadFiles (E:\Intenships\vivasvant systems llp\Web development\node_modules\mocha\lib\mocha.js:331:14)
at Mocha.run (E:\Intenships\vivasvant systems llp\Web development\node_modules\mocha\lib\mocha.js:809:10)
at Object.exports.singleRun (E:\Intenships\vivasvant systems llp\Web development\node_modules\mocha\lib\cli\run-helpers.js:108:16)
at exports.runMocha (E:\Intenships\vivasvant systems llp\Web development\node_modules\mocha\lib\cli\run-helpers.js:142:13)
at Object.exports.handler (E:\Intenships\vivasvant systems llp\Web development\node_modules\mocha\lib\cli\run.js:292:3)
at Object.runCommand (E:\Intenships\vivasvant systems llp\Web development\node_modules\yargs\lib\command.js:242:26)
at Object.parseArgs [as _parseArgs] (E:\Intenships\vivasvant systems llp\Web development\node_modules\yargs\yargs.js:1096:28)
at Object.parse (E:\Intenships\vivasvant systems llp\Web development\node_modules\yargs\yargs.js:575:25)
at Object.exports.main (E:\Intenships\vivasvant systems llp\Web development\node_modules\mocha\lib\cli\cli.js:68:6)
at Object. (E:\Intenships\vivasvant systems llp\Web development\node_modules\mocha\bin\mocha:162:29)
at Module._compile (internal/modules/cjs/loader.js:759:30)
at Object.Module._extensions..js (internal/modules/cjs/loader.js:770:10)
at Module.load (internal/modules/cjs/loader.js:628:32)
at Function.Module._load (internal/modules/cjs/loader.js:555:12)
at Function.Module.runMain (internal/modules/cjs/loader.js:826:10)
at internal/main/run_main_module.js:17:11
npm ERR! Test failed. See above for more details.
You should write your unit test BEFORE you write your app unit code. Otherwise its not TDD.
Hi there,
thanks a lot for the excellent post.
Just an update: faker is no longer available and has become a community project: npm install @faker-js/faker
any mocking methods that don’t use chai ? Chai doesn’t support ES modules. Unit testing is a royal pain in the butt for node.js when needed a mocked database (WAY too complicated for something that should be simple). Just use a real database and be done with it (a staging database)