TypeScript Unit Testing

Unit testing practices in TypeScript projects to ensure code quality.

Unit tests verify code correctness. TypeScript's type system combines perfectly with testing frameworks, allowing you to write type-safe test code.


SVG Diagram: Unit Testing Workflow Background Title TypeScript Unit Testing Workflow Source Code Source Code src/utils/calculator.ts src/services/userService.ts Complete Type Annotations Arrow Test Code Test Code *.test.ts describe() / it() expect() Assertion Arrow Test Result Test Result ✓ Passed / ✗ Failed Coverage Report Error Stack Trace Lower Section: Testing Principles Unit Testing Principles Principle 1 Each test verifies only one thing Principle 2 Arrange-Act-Assert Structure Principle 3 Tests should be independent of each other Arrow Marker

Why Unit Testing Is Needed

Unit testing is an important means of ensuring code quality. It verifies code correctness and prevents bugs.

In TypeScript projects, test code also benefits from the type system: type errors are caught at compile time, the IDE provides intelligent suggestions, and test code is more reliable.

Moreover, test code is also the best documentation; you can understand the expected behavior of functions/classes through tests.

Quality Assurance:Unit tests quickly identify regression issues, ensuring code changes do not break existing functionality.


Test Framework Configuration

Jest is the most popular testing framework for TypeScript projects.

Install Jest

# Install Jest and related dependencies
# - ts-jest:Enable Jest to run TypeScript
# - @types/jest:Type definitions for Jest
npm install --save-dev jest ts-jest @types/jest

# Initialize Jest configuration
npx ts-jest config:init

ts-jest:This is a preprocessor that lets Jest run TypeScript files directly without manual compilation.


Configure jest.config.js

Configure the Jest test environment.

jest.config.js

module.exports = {
    // Use the ts-jest preset
    preset: 'ts-jest',
    // Test environment: node or browser
    testEnvironment: 'node',
    // Test file directory
    roots: ['<rootDir>/src'],
    // Test file matching pattern
    testMatch: ['**/__tests__/**/*.ts'],
    // Supported file extensions
    moduleFileExtensions: ['ts', 'js', 'json'],
    // Files for collecting coverage
    collectCoverageFrom: [
        'src/**/*.ts',
        '!src/**/*.d.ts'  // Exclude type declaration files
    ]
}

Configuration Notes:Test files are usually placed in the __tests__ directory or end with .test.ts.


Test Functions

First, write the business code that needs to be tested.

src/utils/calculator.ts

// Calculator class
export class Calculator {
    // Addition
    add(a: number, b: number): number {
        return a + b;
    }

    // Subtraction
    subtract(a: number, b: number): number {
        return a - b;
    }

    // Multiplication
    multiply(a: number, b: number): number {
        return a * b;
    }

    // Division
    divide(a: number, b: number): number {
        if (b === 0) {
            throw new Error("Cannot divide by zero");
        }
        return a / b;
    }
}

Then write the corresponding test code.

src/utils/calculator.test.ts

import { Calculator } from "./calculator";

// Test suite: Tests for the Calculator class
describe("Calculator", () => {
    let calculator: Calculator;

    // Create a new Calculator instance before each test
    beforeEach(() => {
        calculator = new Calculator();
    });

    // Addition test
    describe("add", () => {
        it("should add two numbers", () => {
            expect(calculator.add(2, 3)).toBe(5);
        });

        it("should handle negative numbers", () => {
            expect(calculator.add(-1, 1)).toBe(0);
        });
    });

    // Division test
    describe("divide", () => {
        it("should divide two numbers", () => {
            expect(calculator.divide(10, 2)).toBe(5);
        });

        it("should throw error when dividing by zero", () => {
            // Expect an error to be thrown
            expect(() => calculator.divide(10, 0)).toThrow();
        });
    });
});

Run Result:

Calculator
  add
    ✓ should add two numbers
    ✓ should handle negative numbers
  divide
    ✓ should divide two numbers
    ✓ should throw error when dividing by zero

describe/it:describe is used to group tests, and it (or test) is used to define individual test cases.


Test Service

Test the business logic of the Service layer.

src/services/userService.ts

// User type
export interface User {
    id: number;
    name: string;
}

// User service class
export class UserService {
    private users: User[] = [];
    private nextId = 1;

    // Create user
    createUser(name: string): User {
        const user = { id: this.nextId++, name };
        this.users.push(user);
        return user;
    }

    // Get user
    getUser(id: number): User | undefined {
        return this.users.find(u => u.id === id);
    }

    // Get all users
    getAllUsers(): User[] {
        return [...this.users];
    }
}

src/services/userService.test.ts

import { UserService } from "./userService";

describe("UserService", () => {
    let service: UserService;

    beforeEach(() => {
        service = new UserService();
    });

    describe("createUser", () => {
        it("should create a user with id", () => {
            const user = service.createUser("Alice");
            expect(user.id).toBe(1);
            expect(user.name).toBe("Alice");
        });

        it("should increment id for each user", () => {
            const user1 = service.createUser("Alice");
            const user2 = service.createUser("Bob");
            expect(user2.id).toBe(user1.id + 1);
        });
    });

    describe("getUser", () => {
        it("should return user by id", () => {
            const created = service.createUser("Alice");
            const found = service.getUser(created.id);
            // Use optional chaining and toBe
            expect(found?.name).toBe("Alice");
        });

        it("should return undefined for non-existent id", () => {
            const found = service.getUser(999);
            expect(found).toBeUndefined();
        });
    });
});

Run Result:

UserService
  createUser
    ✓ should create a user with id
    ✓ should increment id for each user
  getUser
    ✓ should return user by id
    ✓ should return undefined for non-existent id

Test Isolation:Each test case should be independent; use beforeEach to ensure each test has a clean state.


Mock

Use Mocks to simulate dependencies, such as external APIs, databases, etc.

Example

// Mock function: Create a mock function
const mockCallback = jest.fn(x => x * 2);

// Use the mock function
[1, 2, 3].forEach(mockCallback);

// Verify the function was called 3 times
expect(mockCallback).toHaveBeenCalledTimes(3);
// Verify the arguments when the function was called
expect(mockCallback).toHaveBeenCalledWith(2);

// Mock module: Mock an entire module
jest.mock("./api", () => ({
    fetchUser: jest.fn(() => Promise.resolve({ id: 1, name: "Alice" }))
}));

Run Result:

✓ mock 函数被调用 3 次

Purpose of Mocks:When the code under test depends on external systems, using Mocks isolates dependencies so that only the target code's logic is tested.


Notes

  • Test file location:Place in the __tests__ directory or use the .test.ts suffix
  • Test naming:Use descriptive test names that state the expected behavior
  • Independent tests:Each test should run independently without relying on other tests
  • Coverage:Focus on test coverage of core business logic

Best Practices:Tests should be fast, reliable, and independent of each other. Follow the AAA principle: Arrange (prepare), Act (execute), Assert (verify).


Summary

Unit testing is an important means of ensuring TypeScript code quality.

  • Jest:The most popular TypeScript testing framework
  • describe:Used to group related tests
  • it/test:Define individual test cases
  • expect:Assert test results
  • Mock:Mock external dependencies

Recommendations:Write tests for critical business logic to ensure code changes do not introduce bugs.

Other Extensions