How to test Node.js apps with Mocha
Testing Node.js applications with Mocha provides a flexible and feature-rich testing framework with excellent async support. As the creator of CoreUI with over 12 years of Node.js experience since 2014, I’ve used Mocha extensively for backend API testing. Mocha offers a simple, extensible testing interface with support for multiple assertion libraries and reporters. This approach creates comprehensive test suites with clear test organization and detailed error reporting.
Use Mocha to test Node.js applications with flexible test structure, hooks, and async support.
Install Mocha:
npm install --save-dev mocha chai
Configure package.json:
{
"scripts": {
"test": "mocha",
"test:watch": "mocha --watch",
"test:coverage": "nyc mocha"
}
}
Create .mocharc.json:
{
"spec": "test/**/*.test.js",
"require": "test/setup.js",
"timeout": 5000,
"color": true
}
Basic tests with Chai assertions:
// user.service.js
class UserService {
constructor(database) {
this.db = database
}
async createUser(userData) {
if (!userData.email || !userData.name) {
throw new Error('Email and name are required')
}
return await this.db.insert('users', userData)
}
async getUserById(id) {
const user = await this.db.findOne('users', { id })
if (!user) {
throw new Error('User not found')
}
return user
}
isValidEmail(email) {
return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)
}
}
module.exports = UserService
Test file:
// test/user.service.test.js
const { expect } = require('chai')
const sinon = require('sinon')
const UserService = require('../user.service')
describe('UserService', () => {
let service
let mockDatabase
beforeEach(() => {
mockDatabase = {
insert: sinon.stub(),
findOne: sinon.stub()
}
service = new UserService(mockDatabase)
})
afterEach(() => {
sinon.restore()
})
describe('createUser', () => {
it('should create user with valid data', async () => {
const userData = { name: 'John', email: '[email protected]' }
mockDatabase.insert.resolves({ id: 1, ...userData })
const result = await service.createUser(userData)
expect(result).to.deep.include(userData)
expect(mockDatabase.insert).to.have.been.calledWith('users', userData)
})
it('should throw error for missing email', async () => {
const userData = { name: 'John' }
try {
await service.createUser(userData)
expect.fail('Should have thrown error')
} catch (error) {
expect(error.message).to.equal('Email and name are required')
}
})
it('should throw error for missing name', async () => {
const userData = { email: '[email protected]' }
await expect(service.createUser(userData))
.to.be.rejectedWith('Email and name are required')
})
})
describe('getUserById', () => {
it('should return user when found', async () => {
const mockUser = { id: 1, name: 'John' }
mockDatabase.findOne.resolves(mockUser)
const result = await service.getUserById(1)
expect(result).to.deep.equal(mockUser)
expect(mockDatabase.findOne).to.have.been.calledOnce
})
it('should throw error when user not found', async () => {
mockDatabase.findOne.resolves(null)
await expect(service.getUserById(999))
.to.be.rejectedWith('User not found')
})
})
describe('isValidEmail', () => {
it('should validate correct email', () => {
expect(service.isValidEmail('[email protected]')).to.be.true
})
it('should reject invalid emails', () => {
expect(service.isValidEmail('invalid')).to.be.false
expect(service.isValidEmail('')).to.be.false
expect(service.isValidEmail('test@')).to.be.false
})
})
})
Testing async code:
describe('Async operations', () => {
it('handles promises', () => {
return fetchData().then(data => {
expect(data).to.exist
})
})
it('handles async/await', async () => {
const data = await fetchData()
expect(data).to.exist
})
it('handles callbacks with done', (done) => {
fetchDataCallback((err, data) => {
if (err) return done(err)
expect(data).to.exist
done()
})
})
it('handles promise rejection', async () => {
await expect(failingFunction()).to.be.rejected
})
})
Using hooks:
describe('With hooks', () => {
before(() => {
console.log('Runs once before all tests')
})
after(() => {
console.log('Runs once after all tests')
})
beforeEach(() => {
console.log('Runs before each test')
})
afterEach(() => {
console.log('Runs after each test')
})
it('test 1', () => {
expect(true).to.be.true
})
it('test 2', () => {
expect(false).to.be.false
})
})
Skipping and exclusive tests:
describe('Test control', () => {
it.skip('skipped test', () => {
// This test will not run
})
it.only('only this test runs', () => {
expect(true).to.be.true
})
describe.skip('skipped suite', () => {
it('not executed', () => {})
})
})
Testing with timeouts:
describe('Timeouts', () => {
it('completes within time limit', async function() {
this.timeout(2000)
await slowOperation()
})
it('has longer timeout', async function() {
this.timeout(10000)
await verySlowOperation()
})
})
Using Sinon for spies and stubs:
const sinon = require('sinon')
describe('Sinon examples', () => {
it('uses stubs', () => {
const stub = sinon.stub(service, 'getData')
stub.returns({ data: 'mocked' })
const result = service.getData()
expect(result.data).to.equal('mocked')
})
it('uses spies', () => {
const spy = sinon.spy(console, 'log')
service.logMessage('test')
expect(spy).to.have.been.calledWith('test')
})
})
Best Practice Note
Mocha provides flexible test structure with describe and it blocks. Use beforeEach and afterEach for setup and cleanup. Chai provides readable assertions with expect, should, or assert styles. Sinon offers powerful mocking capabilities with stubs, spies, and mocks. For async tests, return promises or use async/await—don’t forget to handle rejections. Use it.only during development to run single tests. This is how we test CoreUI backend services—comprehensive Mocha test suites with Chai assertions and Sinon mocks for maintainable, reliable Node.js APIs.



