본문으로 건너뛰기 Node.js Testing: Jest, Mocha, and Supertest

Node.js Testing: Jest, Mocha, and Supertest

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

  1. Jest: Default all-in-one choice
  2. Mocha + Chai: Flexible stack
  3. Supertest: HTTP-level API tests
  4. Mocks: Isolate I/O and time
  5. Coverage: Enforce thresholds thoughtfully
  6. TDD: Red–green–refactor when it helps

Framework comparison

FrameworkProsCons
JestBatteries included, snapshotsHeavier
MochaMinimal coreNeeds plugins
AVAParallel by defaultSmaller 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

Resources



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.


Other articles related to this topic.


Keywords Covered in This Article (Related Search Terms)

This article covers Node.js, testing, Jest, Mocha, Supertest, TDD, unit-testing.