Node.js Testing: Jest, Mocha, and Supertest
이 글의 핵심
Test Node.js apps with Jest matchers and mocks, async tests, Supertest for Express APIs, MongoDB memory server, integration tests, coverage thresholds, Mocha/Chai, and TDD patterns.
Introduction
Why test?
Testing verifies that code behaves as intended. Benefits:
- ✅ Catch bugs early: Before production
- ✅ Safer refactors: Regression safety net
- ✅ Documentation: Examples of usage
- ✅ Confidence: Ship changes with less fear
- ✅ Maintenance: Saves time over the long run Kinds of tests:
- Unit: Single functions or classes
- Integration: Multiple modules together
- E2E: Full system paths
1. Jest
Install
npm install --save-dev jest
package.json:
{
"scripts": {
"test": "jest",
"test:watch": "jest --watch",
"test:coverage": "jest --coverage"
}
}
First tests
// math.js
function add(a, b) {
return a + b;
}
function subtract(a, b) {
return a - b;
}
module.exports = { add, subtract };
// math.test.js
const { add, subtract } = require('./math');
describe('Math', () => {
test('add sums two numbers', () => {
expect(add(2, 3)).toBe(5);
expect(add(-1, 1)).toBe(0);
expect(add(0, 0)).toBe(0);
});
test('subtract subtracts two numbers', () => {
expect(subtract(5, 3)).toBe(2);
expect(subtract(1, 1)).toBe(0);
expect(subtract(0, 5)).toBe(-5);
});
});
npm test
Matchers
describe('Matcher examples', () => {
test('equality', () => {
expect(2 + 2).toBe(4);
expect({ name: 'Alice' }).toEqual({ name: 'Alice' });
});
test('truthiness', () => {
expect(true).toBeTruthy();
expect(false).toBeFalsy();
expect(null).toBeNull();
expect(undefined).toBeUndefined();
expect('hello').toBeDefined();
});
test('numbers', () => {
expect(10).toBeGreaterThan(5);
expect(10).toBeGreaterThanOrEqual(10);
expect(5).toBeLessThan(10);
expect(0.1 + 0.2).toBeCloseTo(0.3);
});
test('strings', () => {
expect('hello world').toMatch(/world/);
expect('hello').toContain('ell');
});
test('arrays and objects', () => {
const arr = ['apple', 'banana', 'cherry'];
expect(arr).toContain('banana');
expect(arr).toHaveLength(3);
const obj = { name: 'Alice', age: 25 };
expect(obj).toHaveProperty('name');
expect(obj).toHaveProperty('age', 25);
});
test('exceptions', () => {
expect(() => {
throw new Error('oops');
}).toThrow('oops');
});
});
2. Async tests
Promises
// api.js
async function fetchUser(id) {
const response = await fetch(`https://api.example.com/users/${id}`);
return response.json();
}
module.exports = { fetchUser };
// api.test.js
const { fetchUser } = require('./api');
describe('API', () => {
test('fetches a user', async () => {
const user = await fetchUser(1);
expect(user).toHaveProperty('id', 1);
expect(user).toHaveProperty('name');
});
test('missing user rejects', async () => {
await expect(fetchUser(999)).rejects.toThrow();
});
});
Setup and teardown
describe('Database tests', () => {
beforeAll(async () => {
await connectDatabase();
});
beforeEach(async () => {
await clearDatabase();
});
afterEach(async () => {
/* cleanup */
});
afterAll(async () => {
await closeDatabase();
});
test('creates a user', async () => {
const user = await User.create({ name: 'Alice' });
expect(user).toHaveProperty('_id');
});
});
3. Mocks and spies
Mock functions
describe('Mock functions', () => {
test('tracks calls', () => {
const mockFn = jest.fn();
mockFn('hello');
mockFn('world');
expect(mockFn).toHaveBeenCalledTimes(2);
expect(mockFn).toHaveBeenCalledWith('hello');
expect(mockFn).toHaveBeenLastCalledWith('world');
});
test('return values', () => {
const mockFn = jest.fn();
mockFn.mockReturnValue(42);
expect(mockFn()).toBe(42);
mockFn.mockReturnValueOnce(1).mockReturnValueOnce(2).mockReturnValue(3);
expect(mockFn()).toBe(1);
expect(mockFn()).toBe(2);
expect(mockFn()).toBe(3);
expect(mockFn()).toBe(3);
});
});
Module mocks
// user.js
const axios = require('axios');
async function getUser(id) {
const response = await axios.get(`https://api.example.com/users/${id}`);
return response.data;
}
module.exports = { getUser };
// user.test.js
const axios = require('axios');
const { getUser } = require('./user');
jest.mock('axios');
describe('getUser', () => {
test('returns user data', async () => {
const mockUser = { id: 1, name: 'Alice' };
axios.get.mockResolvedValue({ data: mockUser });
const user = await getUser(1);
expect(axios.get).toHaveBeenCalledWith('https://api.example.com/users/1');
expect(user).toEqual(mockUser);
});
test('propagates errors', async () => {
axios.get.mockRejectedValue(new Error('Network Error'));
await expect(getUser(1)).rejects.toThrow('Network Error');
});
});
Spies
describe('Spies', () => {
test('wraps a method', () => {
const obj = { method: () => 'original' };
const spy = jest.spyOn(obj, 'method');
obj.method();
expect(spy).toHaveBeenCalled();
spy.mockRestore();
});
});
4. API tests with Supertest
npm install --save-dev supertest
// app.js
const express = require('express');
const app = express();
app.use(express.json());
app.get('/api/users', (req, res) => {
res.json({ users: [] });
});
app.post('/api/users', (req, res) => {
const { name, email } = req.body;
if (!name || !email) {
return res.status(400).json({ error: 'Name and email are required' });
}
res.status(201).json({ id: 1, name, email });
});
module.exports = app;
// app.test.js
const request = require('supertest');
const app = require('./app');
describe('API', () => {
describe('GET /api/users', () => {
test('returns users', async () => {
const response = await request(app)
.get('/api/users')
.expect(200)
.expect('Content-Type', /json/);
expect(response.body).toHaveProperty('users');
expect(Array.isArray(response.body.users)).toBe(true);
});
});
describe('POST /api/users', () => {
test('creates a user', async () => {
const newUser = { name: 'Alice', email: 'alice@example.com' };
const response = await request(app)
.post('/api/users')
.send(newUser)
.expect(201)
.expect('Content-Type', /json/);
expect(response.body).toMatchObject(newUser);
expect(response.body).toHaveProperty('id');
});
test('returns 400 when invalid', async () => {
const response = await request(app)
.post('/api/users')
.send({ name: 'Alice' })
.expect(400);
expect(response.body).toHaveProperty('error');
});
});
});
5. Database tests
MongoDB setup
// test/setup.js
const mongoose = require('mongoose');
const { MongoMemoryServer } = require('mongodb-memory-server');
let mongoServer;
beforeAll(async () => {
mongoServer = await MongoMemoryServer.create();
await mongoose.connect(mongoServer.getUri());
});
afterAll(async () => {
await mongoose.disconnect();
await mongoServer.stop();
});
afterEach(async () => {
const collections = mongoose.connection.collections;
for (const key in collections) {
await collections[key].deleteMany();
}
});
jest.config.js:
module.exports = {
testEnvironment: 'node',
setupFilesAfterEnv: ['<rootDir>/test/setup.js'],
coveragePathIgnorePatterns: ['/node_modules/']
};
Model tests
// models/User.test.js
const User = require('../models/User');
describe('User model', () => {
test('creates a user', async () => {
const userData = {
name: 'Alice',
email: 'alice@example.com',
password: 'password123'
};
const user = await User.create(userData);
expect(user).toHaveProperty('_id');
expect(user.name).toBe(userData.name);
expect(user.email).toBe(userData.email);
});
test('duplicate email fails', async () => {
await User.create({
name: 'Alice',
email: 'alice@example.com',
password: 'password123'
});
await expect(User.create({
name: 'Bob',
email: 'alice@example.com',
password: 'password456'
})).rejects.toThrow();
});
test('required fields enforced', async () => {
await expect(User.create({ name: 'Alice' })).rejects.toThrow();
});
});
6. Integration tests
// test/integration/users.test.js
const request = require('supertest');
const app = require('../../app');
const User = require('../../models/User');
describe('Users API integration', () => {
describe('POST /api/users', () => {
test('creates a user', async () => {
const userData = {
name: 'Alice',
email: 'alice@example.com',
password: 'password123'
};
const response = await request(app)
.post('/api/users')
.send(userData)
.expect(201);
expect(response.body).toHaveProperty('id');
expect(response.body.name).toBe(userData.name);
const user = await User.findById(response.body.id);
expect(user).toBeTruthy();
expect(user.email).toBe(userData.email);
});
});
describe('GET /api/users/:id', () => {
test('returns a user', async () => {
const user = await User.create({
name: 'Alice',
email: 'alice@example.com',
password: 'password123'
});
const response = await request(app)
.get(`/api/users/${user._id}`)
.expect(200);
expect(response.body.name).toBe(user.name);
expect(response.body.email).toBe(user.email);
});
test('404 when missing', async () => {
const response = await request(app)
.get('/api/users/507f1f77bcf86cd799439011')
.expect(404);
expect(response.body).toHaveProperty('error');
});
});
});
Auth integration
const request = require('supertest');
const app = require('../../app');
const User = require('../../models/User');
const bcrypt = require('bcrypt');
describe('Auth API', () => {
describe('POST /auth/login', () => {
test('logs in with valid credentials', async () => {
const password = 'password123';
await User.create({
name: 'Alice',
email: 'alice@example.com',
password: await bcrypt.hash(password, 10)
});
const response = await request(app)
.post('/auth/login')
.send({ email: 'alice@example.com', password })
.expect(200);
expect(response.body).toHaveProperty('token');
expect(response.body.user.email).toBe('alice@example.com');
});
test('401 on wrong password', async () => {
await User.create({
name: 'Alice',
email: 'alice@example.com',
password: await bcrypt.hash('password123', 10)
});
await request(app)
.post('/auth/login')
.send({ email: 'alice@example.com', password: 'wrong' })
.expect(401);
});
});
describe('GET /api/profile', () => {
test('returns profile with token', async () => {
await User.create({
name: 'Alice',
email: 'alice@example.com',
password: await bcrypt.hash('password123', 10)
});
const loginResponse = await request(app)
.post('/auth/login')
.send({ email: 'alice@example.com', password: 'password123' });
const token = loginResponse.body.token;
const response = await request(app)
.get('/api/profile')
.set('Authorization', `Bearer ${token}`)
.expect(200);
expect(response.body.user.email).toBe('alice@example.com');
});
test('401 without token', async () => {
await request(app).get('/api/profile').expect(401);
});
});
});
7. Mocha + Chai
npm install --save-dev mocha chai
{ "scripts": { "test": "mocha" } }
// 변수 선언 및 초기화
const { expect } = require('chai');
const { add, subtract } = require('../math');
describe('Math', () => {
describe('add()', () => {
it('adds two numbers', () => {
expect(add(2, 3)).to.equal(5);
});
});
describe('subtract()', () => {
it('subtracts two numbers', () => {
expect(subtract(5, 3)).to.equal(2);
});
});
});
Chai
const { expect } = require('chai');
describe('Chai', () => {
it('equality', () => {
expect(2 + 2).to.equal(4);
expect({ name: 'Alice' }).to.deep.equal({ name: 'Alice' });
});
it('types', () => {
expect('hello').to.be.a('string');
expect(123).to.be.a('number');
expect([]).to.be.an('array');
});
});
8. Coverage
npm test -- --coverage
jest.config.js:
module.exports = {
collectCoverageFrom: [
'src/**/*.js',
'!src/**/*.test.js',
'!src/index.js'
],
coverageThreshold: {
global: {
branches: 80,
functions: 80,
lines: 80,
statements: 80
}
}
};
9. Practical examples
User service
// services/userService.js
const User = require('../models/User');
const bcrypt = require('bcrypt');
class UserService {
async createUser(userData) {
const { email, password, name } = userData;
const existing = await User.findOne({ email });
if (existing) {
throw new Error('Email already registered');
}
const hashedPassword = await bcrypt.hash(password, 10);
return User.create({ email, password: hashedPassword, name });
}
async getUserById(id) {
const user = await User.findById(id).select('-password');
if (!user) throw new Error('User not found');
return user;
}
async updateUser(id, updates) {
const user = await User.findByIdAndUpdate(id, updates, {
new: true,
runValidators: true
}).select('-password');
if (!user) throw new Error('User not found');
return user;
}
async deleteUser(id) {
const user = await User.findByIdAndDelete(id);
if (!user) throw new Error('User not found');
return user;
}
}
module.exports = new UserService();
const userService = require('./userService');
const User = require('../models/User');
describe('UserService', () => {
describe('createUser', () => {
test('creates user', async () => {
const userData = {
name: 'Alice',
email: 'alice@example.com',
password: 'password123'
};
const user = await userService.createUser(userData);
expect(user).toHaveProperty('_id');
expect(user.password).not.toBe(userData.password);
});
test('duplicate email', async () => {
await userService.createUser({
name: 'Alice',
email: 'alice@example.com',
password: 'password123'
});
await expect(userService.createUser({
name: 'Bob',
email: 'alice@example.com',
password: 'password456'
})).rejects.toThrow('Email already registered');
});
});
});
Auth API (extended)
See integration example above; add JWT verification checks that match your auth middleware.
10. Test doubles
Mock (email)
jest.mock('nodemailer');
describe('sendEmail', () => {
test('sends mail', async () => {
const mockSendMail = jest.fn().mockResolvedValue({ messageId: '123' });
nodemailer.createTransport.mockReturnValue({ sendMail: mockSendMail });
await sendEmail('test@example.com', 'Subject', 'Body');
expect(mockSendMail).toHaveBeenCalledWith({
to: 'test@example.com',
subject: 'Subject',
text: 'Body'
});
});
});
Stub (payment)
jest.mock('axios');
describe('processPayment', () => {
test('returns gateway data', async () => {
axios.post.mockResolvedValue({
data: { success: true, transactionId: 'txn_123' }
});
const result = await processPayment(10000);
expect(result.success).toBe(true);
expect(result.transactionId).toBe('txn_123');
});
});
11. Common issues
Async timeout
Timeout - Async callback was not invoked within the 5000 ms timeout
test('slow op', async () => {
await slowOperation();
}, 10000);
// jest.config.js
module.exports = { testTimeout: 10000 };
Shared state
Use beforeEach to reset fixtures; avoid module-level mutable arrays unless cleared.
DB cleanup
afterEach(async () => {
await User.deleteMany({});
await Post.deleteMany({});
});
12. Practical tips
Layout
test/
├── unit/
├── integration/
├── e2e/
└── setup.js
Helpers
async function createTestUser(overrides = {}) {
const defaults = {
name: 'Test User',
email: 'test@example.com',
password: await bcrypt.hash('password123', 10),
role: 'user'
};
return User.create({ ...defaults, ...overrides });
}
TDD sketch
Write a failing test, minimal pass, refactor, add edge-case tests.
Summary
Takeaways
- Jest: Default all-in-one choice
- Mocha + Chai: Flexible stack
- Supertest: HTTP-level API tests
- Mocks: Isolate I/O and time
- Coverage: Enforce thresholds thoughtfully
- TDD: Red–green–refactor when it helps
Framework comparison
| Framework | Pros | Cons |
|---|---|---|
| Jest | Batteries included, snapshots | Heavier |
| Mocha | Minimal core | Needs plugins |
| AVA | Parallel by default | Smaller ecosystem |
Practices
- Independent tests
- One main assertion per test (rule of thumb)
- Descriptive names
- Arrange–Act–Assert
- Test failure paths
- Mock external systems
Next steps
- Node.js deployment
- Node.js performance
- CI/CD pipelines
Resources
Related posts
Frequently Asked Questions (FAQ)
Q. When would I use this in practice?
A. Test Node.js apps with Jest matchers and mocks, async tests, Supertest for Express APIs, MongoDB memory server, integrat…
Q. What should I read before this?
A. Follow the previous article or related articles links at the bottom of each post to learn in sequence.
Q. Where can I study this more deeply?
A. Check cppreference and the relevant library’s official documentation. The reference links at the end of the article are also worth using.
Related Articles (Internal Links)
Other articles related to this topic.
- Express.js Complete Guide: Node.js Web Framework and REST
- Node.js Authentication and Security: JWT, bcrypt, Sessions,
- Kotlin 테스팅 | JUnit, MockK, 테스트 작성법
Keywords Covered in This Article (Related Search Terms)
This article covers Node.js, testing, Jest, Mocha, Supertest, TDD, unit-testing.