🏠 Home / Hub

🟢 Node.js Lesson 09 — Testing with Jest

← Back to Node.js Menu  |  🏠 Hub

1. Setup

npm install --save-dev jest supertest

# package.json
{
  "scripts": {
    "test":       "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  },
  "jest": {
    "testEnvironment": "node",
    "testMatch": ["**/__tests__/**/*.js", "**/*.test.js"],
    "collectCoverageFrom": ["src/**/*.js"]
  }
}

# File naming
src/utils/math.js
src/utils/math.test.js    ← Jest auto-finds these
# OR
src/__tests__/math.test.js

2. Unit Tests

// src/utils/math.js
function add(a, b) { return a + b; }
function divide(a, b) {
  if (b === 0) throw new Error("Cannot divide by zero");
  return a / b;
}
function factorial(n) {
  if (n < 0)  throw new Error("Negative number");
  if (n === 0) return 1;
  return n * factorial(n - 1);
}
module.exports = { add, divide, factorial };

// src/utils/math.test.js
const { add, divide, factorial } = require('./math');

describe('Math utils', () => {

  describe('add()', () => {
    test('adds two positive numbers', () => {
      expect(add(2, 3)).toBe(5);
    });
    test('adds negative numbers', () => {
      expect(add(-1, -2)).toBe(-3);
    });
    test('adds zero', () => {
      expect(add(5, 0)).toBe(5);
    });
  });

  describe('divide()', () => {
    test('divides correctly', () => {
      expect(divide(10, 2)).toBe(5);
      expect(divide(7, 2)).toBeCloseTo(3.5);
    });
    test('throws on division by zero', () => {
      expect(() => divide(10, 0)).toThrow("Cannot divide by zero");
    });
  });

  describe('factorial()', () => {
    test.each([
      [0, 1],
      [1, 1],
      [5, 120],
      [10, 3628800],
    ])('factorial(%i) = %i', (input, expected) => {
      expect(factorial(input)).toBe(expected);
    });

    test('throws on negative', () => {
      expect(() => factorial(-1)).toThrow();
    });
  });
});

3. API Testing with Supertest

// src/routes/users.js (simplified)
const express = require('express');
const router  = express.Router();
const users   = [];

router.get('/',    (req, res) => res.json(users));
router.post('/',   (req, res) => {
  const { name, email } = req.body;
  if (!name || !email) return res.status(400).json({ error: 'Missing fields' });
  const user = { id: Date.now(), name, email };
  users.push(user);
  res.status(201).json(user);
});
router.get('/:id', (req, res) => {
  const user = users.find(u => u.id == req.params.id);
  user ? res.json(user) : res.status(404).json({ error: 'Not found' });
});
module.exports = router;

// app.js (separate from server.js for testability)
const express = require('express');
const app = express();
app.use(express.json());
app.use('/api/users', require('./routes/users'));
module.exports = app;

// src/__tests__/users.test.js
const request = require('supertest');
const app     = require('../app');

describe('Users API', () => {
  let createdId;

  test('GET /api/users — returns empty array', async () => {
    const res = await request(app).get('/api/users');
    expect(res.status).toBe(200);
    expect(res.body).toBeInstanceOf(Array);
  });

  test('POST /api/users — creates user', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: 'Ko Ko', email: 'ko@example.com' });

    expect(res.status).toBe(201);
    expect(res.body).toMatchObject({ name: 'Ko Ko', email: 'ko@example.com' });
    expect(res.body.id).toBeDefined();
    createdId = res.body.id;
  });

  test('POST /api/users — 400 on missing fields', async () => {
    const res = await request(app)
      .post('/api/users')
      .send({ name: 'No Email' });

    expect(res.status).toBe(400);
    expect(res.body.error).toBeTruthy();
  });

  test('GET /api/users/:id — finds user', async () => {
    const res = await request(app).get(`/api/users/${createdId}`);
    expect(res.status).toBe(200);
    expect(res.body.name).toBe('Ko Ko');
  });

  test('GET /api/users/:id — 404 when not found', async () => {
    const res = await request(app).get('/api/users/99999');
    expect(res.status).toBe(404);
  });
});

4. Mocking

// Mock a module
jest.mock('../utils/emailService');
const emailService = require('../utils/emailService');

test('sends welcome email on register', async () => {
  emailService.send = jest.fn().mockResolvedValue(true);

  await registerUser({ name: 'Ko Ko', email: 'ko@test.com' });

  expect(emailService.send).toHaveBeenCalledTimes(1);
  expect(emailService.send).toHaveBeenCalledWith(
    expect.objectContaining({ to: 'ko@test.com' })
  );
});

// Mock Date.now()
jest.spyOn(Date, 'now').mockReturnValue(1234567890);

// Restore after test
afterEach(() => { jest.restoreAllMocks(); });

// Before/After hooks
beforeAll(async ()  => { /* setup DB */ });
afterAll(async ()   => { /* teardown DB */ });
beforeEach(async () => { /* reset state */ });
afterEach(async ()  => { /* cleanup */ });

5. Common Matchers

MatcherTests
toBe(val)Strict equality (===)
toEqual(obj)Deep equality (objects)
toMatchObject(obj)Partial match (subset)
toBeTruthy() / toBeFalsy()truthy/falsy
toBeNull() / toBeUndefined()null/undefined
toBeGreaterThan(n)number comparison
toBeCloseTo(n)float comparison
toContain(item)array/string contains
toHaveLength(n)array/string length
toThrow(msg)function throws
toHaveBeenCalled()mock was called
toHaveBeenCalledWith(...)mock called with args
resolves / rejectsasync: await expect(fn()).resolves.toBe(x)
💡 Run: npm test -- --verbose (see all test names) | --coverage (see coverage report)

← Node.js 08  |  Next: Node.js 10 → Docker & Deploy →

📌 Study Checklist