Skip to main content
โšก Calmops

Unit Testing with Jest

Unit Testing with Jest

Jest is a popular JavaScript testing framework that makes writing and running tests easy and enjoyable.

Installation and Setup

Install Jest

npm install --save-dev jest

Configure package.json

{
  "scripts": {
    "test": "jest",
    "test:watch": "jest --watch",
    "test:coverage": "jest --coverage"
  }
}

Create Test File

// math.test.js
describe("Math operations", () => {
    test("adds two numbers", () => {
        expect(2 + 2).toBe(4);
    });
});

Run Tests

npm test

Basic Test Structure

Test Syntax

test("description", () => {
    // Test code
});

// Or using it()
it("description", () => {
    // Test code
});

Describe Blocks

describe("Calculator", () => {
    test("adds numbers", () => {
        expect(add(2, 3)).toBe(5);
    });
    
    test("subtracts numbers", () => {
        expect(subtract(5, 3)).toBe(2);
    });
});

Matchers

Equality

test("equality matchers", () => {
    expect(4).toBe(4); // Strict equality
    expect({ a: 1 }).toEqual({ a: 1 }); // Deep equality
    expect(null).toBeNull();
    expect(undefined).toBeUndefined();
    expect(true).toBeDefined();
});

Truthiness

test("truthiness", () => {
    expect(true).toBeTruthy();
    expect(false).toBeFalsy();
    expect(1).toBeTruthy();
    expect(0).toBeFalsy();
});

Numbers

test("number matchers", () => {
    expect(4).toBeGreaterThan(3);
    expect(3).toBeGreaterThanOrEqual(3);
    expect(2).toBeLessThan(3);
    expect(3).toBeLessThanOrEqual(3);
    expect(0.1 + 0.2).toBeCloseTo(0.3);
});

Strings

test("string matchers", () => {
    expect("hello").toMatch(/ell/);
    expect("hello").toMatch("ell");
    expect("hello").toContain("ell");
});

Arrays and Objects

test("array and object matchers", () => {
    expect([1, 2, 3]).toContain(2);
    expect([1, 2, 3]).toHaveLength(3);
    expect({ a: 1 }).toHaveProperty("a");
    expect({ a: 1 }).toHaveProperty("a", 1);
});

Exceptions

test("exception matchers", () => {
    expect(() => {
        throw new Error("Test error");
    }).toThrow();
    
    expect(() => {
        throw new Error("Test error");
    }).toThrow("Test error");
    
    expect(() => {
        throw new Error("Test error");
    }).toThrow(Error);
});

Setup and Teardown

beforeEach and afterEach

describe("Database", () => {
    let db;
    
    beforeEach(() => {
        db = new Database();
        db.connect();
    });
    
    afterEach(() => {
        db.disconnect();
    });
    
    test("saves data", () => {
        db.save("key", "value");
        expect(db.get("key")).toBe("value");
    });
});

beforeAll and afterAll

describe("API", () => {
    let server;
    
    beforeAll(() => {
        server = startServer();
    });
    
    afterAll(() => {
        server.stop();
    });
    
    test("fetches data", async () => {
        const data = await fetch("/api/data");
        expect(data).toBeDefined();
    });
});

Practical Examples

Testing Functions

// math.js
export function add(a, b) {
    return a + b;
}

export function multiply(a, b) {
    return a * b;
}

// math.test.js
import { add, multiply } from "./math";

describe("Math functions", () => {
    test("add returns sum", () => {
        expect(add(2, 3)).toBe(5);
        expect(add(-1, 1)).toBe(0);
        expect(add(0, 0)).toBe(0);
    });
    
    test("multiply returns product", () => {
        expect(multiply(2, 3)).toBe(6);
        expect(multiply(-2, 3)).toBe(-6);
        expect(multiply(0, 5)).toBe(0);
    });
});

Testing Classes

// User.js
export class User {
    constructor(name, email) {
        this.name = name;
        this.email = email;
    }
    
    getInfo() {
        return `${this.name} (${this.email})`;
    }
    
    isValidEmail() {
        return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(this.email);
    }
}

// User.test.js
import { User } from "./User";

describe("User class", () => {
    let user;
    
    beforeEach(() => {
        user = new User("Alice", "[email protected]");
    });
    
    test("creates user with name and email", () => {
        expect(user.name).toBe("Alice");
        expect(user.email).toBe("[email protected]");
    });
    
    test("getInfo returns formatted string", () => {
        expect(user.getInfo()).toBe("Alice ([email protected])");
    });
    
    test("isValidEmail validates email", () => {
        expect(user.isValidEmail()).toBe(true);
        
        user.email = "invalid";
        expect(user.isValidEmail()).toBe(false);
    });
});

Testing Async Functions

// api.js
export async function fetchUser(id) {
    const response = await fetch(`/api/users/${id}`);
    return response.json();
}

// api.test.js
import { fetchUser } from "./api";

describe("API functions", () => {
    test("fetchUser returns user data", async () => {
        const user = await fetchUser(1);
        expect(user).toHaveProperty("id");
        expect(user).toHaveProperty("name");
    });
});

Testing with Mocks

// database.js
export class Database {
    async save(key, value) {
        // Actual database operation
    }
}

// service.js
export class UserService {
    constructor(db) {
        this.db = db;
    }
    
    async createUser(name, email) {
        const user = { name, email };
        await this.db.save("user", user);
        return user;
    }
}

// service.test.js
import { UserService } from "./service";

describe("UserService", () => {
    test("createUser saves to database", async () => {
        const mockDb = {
            save: jest.fn()
        };
        
        const service = new UserService(mockDb);
        const user = await service.createUser("Alice", "[email protected]");
        
        expect(mockDb.save).toHaveBeenCalledWith("user", user);
        expect(mockDb.save).toHaveBeenCalledTimes(1);
    });
});

Testing with Spies

// calculator.js
export class Calculator {
    add(a, b) {
        return a + b;
    }
    
    calculate(a, b, operation) {
        return operation(a, b);
    }
}

// calculator.test.js
import { Calculator } from "./calculator";

describe("Calculator", () => {
    test("calculate calls operation", () => {
        const calc = new Calculator();
        const spy = jest.spyOn(calc, "add");
        
        calc.calculate(2, 3, calc.add);
        
        expect(spy).toHaveBeenCalledWith(2, 3);
        spy.mockRestore();
    });
});

Test Coverage

Generate Coverage Report

npm run test:coverage

Coverage Thresholds

{
  "jest": {
    "collectCoverageFrom": [
      "src/**/*.js",
      "!src/index.js"
    ],
    "coverageThreshold": {
      "global": {
        "branches": 80,
        "functions": 80,
        "lines": 80,
        "statements": 80
      }
    }
  }
}

Best Practices

Write Descriptive Test Names

// Good
test("returns sum of two positive numbers", () => {
    expect(add(2, 3)).toBe(5);
});

// Avoid
test("add works", () => {
    expect(add(2, 3)).toBe(5);
});

Test One Thing Per Test

// Good - focused test
test("add returns correct sum", () => {
    expect(add(2, 3)).toBe(5);
});

// Avoid - testing multiple things
test("add and multiply work", () => {
    expect(add(2, 3)).toBe(5);
    expect(multiply(2, 3)).toBe(6);
});

Use Arrange-Act-Assert Pattern

test("user can login", () => {
    // Arrange
    const user = new User("alice", "password123");
    
    // Act
    const result = user.login("alice", "password123");
    
    // Assert
    expect(result).toBe(true);
});

Mock External Dependencies

// Good - mock external API
jest.mock("./api");

test("service fetches data", async () => {
    const mockData = { id: 1, name: "Alice" };
    api.fetchUser.mockResolvedValue(mockData);
    
    const result = await service.getUser(1);
    expect(result).toEqual(mockData);
});

Summary

  • Jest: JavaScript testing framework
  • test(): define a test
  • expect(): make assertions
  • Matchers: toBe(), toEqual(), toContain(), etc.
  • Setup/Teardown: beforeEach(), afterEach()
  • Mocks: jest.fn(), jest.mock()
  • Spies: jest.spyOn()
  • Coverage: measure code coverage
  • Best practice: write focused, descriptive tests

Official Documentation

Next Steps

  1. Test Fixtures and Setup/Teardown
  2. Mocking and Spying with Jest
  3. Code Coverage and Quality Metrics

Comments