+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 222 of 355

๐Ÿงช Test-Driven Development: TDD with TypeScript

Master test-driven development: tdd with typescript in TypeScript with practical examples, best practices, and real-world applications ๐Ÿš€

๐Ÿš€Intermediate
25 min read

Prerequisites

  • Basic understanding of JavaScript ๐Ÿ“
  • TypeScript installation โšก
  • VS Code or preferred IDE ๐Ÿ’ป

What you'll learn

  • Understand TDD fundamentals and the Red-Green-Refactor cycle ๐ŸŽฏ
  • Apply TDD methodology in TypeScript projects ๐Ÿ—๏ธ
  • Debug common TDD issues and maintain test quality ๐Ÿ›
  • Write type-safe tests and testable code โœจ

๐ŸŽฏ Introduction

Welcome to the exciting world of Test-Driven Development with TypeScript! ๐ŸŽ‰ In this guide, weโ€™ll explore how TDD transforms your development process from a chaotic scramble into a confident, systematic approach.

Youโ€™ll discover how TDD can revolutionize your TypeScript development experience. Whether youโ€™re building web applications ๐ŸŒ, server-side APIs ๐Ÿ–ฅ๏ธ, or libraries ๐Ÿ“š, understanding TDD is essential for writing robust, maintainable code with confidence.

By the end of this tutorial, youโ€™ll feel confident implementing TDD in your own TypeScript projects! Letโ€™s dive in! ๐ŸŠโ€โ™‚๏ธ

๐Ÿ“š Understanding Test-Driven Development

๐Ÿค” What is Test-Driven Development?

TDD is like building a house with a blueprint first ๐Ÿ—๏ธ. Think of it as writing the โ€œwhat should happenโ€ before writing the โ€œhow it happensโ€ - you define your expectations, then make them come true!

In TypeScript terms, TDD means writing tests that fail, then writing just enough code to make them pass, then improving the code while keeping tests green ๐ŸŸข. This means you can:

  • โœจ Catch bugs before they happen
  • ๐Ÿš€ Refactor fearlessly with confidence
  • ๐Ÿ›ก๏ธ Build exactly whatโ€™s needed, nothing more

๐Ÿ’ก Why Use TDD?

Hereโ€™s why developers love TDD:

  1. Confidence ๐Ÿ”’: Your tests act as a safety net
  2. Better Design ๐Ÿ’ป: Forces you to think about interfaces first
  3. Living Documentation ๐Ÿ“–: Tests show how code should behave
  4. Regression Prevention ๐Ÿ”ง: Changes wonโ€™t break existing functionality

Real-world example: Imagine building a shopping cart ๐Ÿ›’. With TDD, youโ€™d first write tests for โ€œadding items should increase the totalโ€, then implement the functionality to make those tests pass!

๐Ÿ”ง Basic TDD Cycle: Red-Green-Refactor

๐Ÿ“ The Sacred Cycle

TDD follows a simple but powerful cycle:

// ๐Ÿ”ด RED: Write a failing test
describe("Calculator", () => {
  it("should add two numbers correctly", () => {
    const calc = new Calculator();
    expect(calc.add(2, 3)).toBe(5); // ๐Ÿ’ฅ This will fail - Calculator doesn't exist!
  });
});

// ๐ŸŸข GREEN: Write minimal code to pass
class Calculator {
  add(a: number, b: number): number {
    return a + b; // โœ… Just enough to make it pass!
  }
}

// ๐Ÿ”ต REFACTOR: Improve the code while keeping tests green
class Calculator {
  // ๐ŸŽจ Add type safety and validation
  add(a: number, b: number): number {
    if (typeof a !== 'number' || typeof b !== 'number') {
      throw new Error('๐Ÿšซ Only numbers allowed!');
    }
    return a + b;
  }
}

๐Ÿ’ก Explanation: Notice how we start with the test, then build the minimum code to pass it, then improve! This ensures we only write what we need.

๐ŸŽฏ Setting Up Your TypeScript Testing Environment

Letโ€™s set up a modern testing environment:

# ๐Ÿ“ฆ Install Jest and TypeScript support
npm install --save-dev jest @types/jest ts-jest typescript

# ๐Ÿ”ง Initialize Jest configuration
npx ts-jest config:init
// ๐Ÿ“ jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  roots: ['<rootDir>/src'],
  testMatch: ['**/__tests__/**/*.ts', '**/*.test.ts'],
  collectCoverageFrom: [
    'src/**/*.ts',
    '!src/**/*.d.ts',
  ],
};

๐Ÿ’ก Practical Examples

๐Ÿ›’ Example 1: TDD Shopping Cart

Letโ€™s build a shopping cart using TDD:

// ๐Ÿ”ด RED: Start with the test
describe("ShoppingCart", () => {
  it("should start empty", () => {
    const cart = new ShoppingCart();
    expect(cart.getItemCount()).toBe(0);
    expect(cart.getTotal()).toBe(0);
  });

  it("should add items correctly", () => {
    const cart = new ShoppingCart();
    const item = { id: "1", name: "TypeScript Book ๐Ÿ“˜", price: 29.99 };
    
    cart.addItem(item);
    
    expect(cart.getItemCount()).toBe(1);
    expect(cart.getTotal()).toBe(29.99);
  });
});

// ๐ŸŸข GREEN: Implement to make tests pass
interface CartItem {
  id: string;
  name: string;
  price: number;
}

class ShoppingCart {
  private items: CartItem[] = [];

  addItem(item: CartItem): void {
    this.items.push(item);
  }

  getItemCount(): number {
    return this.items.length;
  }

  getTotal(): number {
    return this.items.reduce((sum, item) => sum + item.price, 0);
  }
}

// ๐Ÿ”ต REFACTOR: Add more functionality through TDD
describe("ShoppingCart advanced features", () => {
  it("should remove items correctly", () => {
    const cart = new ShoppingCart();
    const item = { id: "1", name: "Coffee โ˜•", price: 4.99 };
    
    cart.addItem(item);
    cart.removeItem(item.id);
    
    expect(cart.getItemCount()).toBe(0);
    expect(cart.getTotal()).toBe(0);
  });

  it("should handle quantities", () => {
    const cart = new ShoppingCart();
    const item = { id: "1", name: "Cookies ๐Ÿช", price: 2.99 };
    
    cart.addItem(item, 3); // ๐ŸŽฏ Add quantity parameter
    
    expect(cart.getItemCount()).toBe(3);
    expect(cart.getTotal()).toBe(8.97);
  });
});

๐ŸŽฏ Try it yourself: Add a discount system that applies percentage discounts!

๐ŸŽฎ Example 2: TDD Game Score System

Letโ€™s create a game scoring system:

// ๐Ÿ”ด RED: Define what we want first
describe("GameScore", () => {
  it("should track player scores", () => {
    const game = new GameScore();
    
    game.addPlayer("Alice ๐Ÿ‘ฉ");
    game.addPoints("Alice ๐Ÿ‘ฉ", 100);
    
    expect(game.getScore("Alice ๐Ÿ‘ฉ")).toBe(100);
  });

  it("should determine the winner", () => {
    const game = new GameScore();
    
    game.addPlayer("Alice ๐Ÿ‘ฉ");
    game.addPlayer("Bob ๐Ÿ‘จ");
    game.addPoints("Alice ๐Ÿ‘ฉ", 100);
    game.addPoints("Bob ๐Ÿ‘จ", 150);
    
    expect(game.getWinner()).toBe("Bob ๐Ÿ‘จ");
  });

  it("should handle achievements", () => {
    const game = new GameScore();
    
    game.addPlayer("Charlie ๐Ÿง‘");
    game.addPoints("Charlie ๐Ÿง‘", 1000); // ๐Ÿ† Big achievement!
    
    expect(game.getAchievements("Charlie ๐Ÿง‘")).toContain("๐Ÿ† High Scorer");
  });
});

// ๐ŸŸข GREEN: Implement the functionality
interface Player {
  name: string;
  score: number;
  achievements: string[];
}

class GameScore {
  private players: Map<string, Player> = new Map();

  addPlayer(name: string): void {
    this.players.set(name, {
      name,
      score: 0,
      achievements: ["๐ŸŒŸ New Player"]
    });
  }

  addPoints(name: string, points: number): void {
    const player = this.players.get(name);
    if (!player) throw new Error(`๐Ÿšซ Player ${name} not found!`);
    
    player.score += points;
    
    // ๐ŸŽŠ Check for achievements
    if (player.score >= 1000 && !player.achievements.includes("๐Ÿ† High Scorer")) {
      player.achievements.push("๐Ÿ† High Scorer");
    }
  }

  getScore(name: string): number {
    const player = this.players.get(name);
    return player ? player.score : 0;
  }

  getWinner(): string | null {
    let winner: Player | null = null;
    
    for (const player of this.players.values()) {
      if (!winner || player.score > winner.score) {
        winner = player;
      }
    }
    
    return winner ? winner.name : null;
  }

  getAchievements(name: string): string[] {
    const player = this.players.get(name);
    return player ? player.achievements : [];
  }
}

๐Ÿš€ Advanced TDD Concepts

๐Ÿง™โ€โ™‚๏ธ Mocking and Test Doubles

When testing code that depends on external services, use mocks:

// ๐ŸŽฏ Interface for testing
interface UserRepository {
  save(user: User): Promise<void>;
  findById(id: string): Promise<User | null>;
}

// ๐ŸŽฎ Service to test
class UserService {
  constructor(private repo: UserRepository) {}

  async createUser(userData: { name: string; email: string }): Promise<User> {
    const user = new User(userData.name, userData.email);
    await this.repo.save(user);
    return user;
  }
}

// ๐Ÿงช Test with mock
describe("UserService", () => {
  it("should create and save user", async () => {
    // ๐ŸŽญ Create a mock repository
    const mockRepo: UserRepository = {
      save: jest.fn(),
      findById: jest.fn()
    };

    const service = new UserService(mockRepo);
    const userData = { name: "Alice ๐Ÿ‘ฉ", email: "[email protected]" };

    const user = await service.createUser(userData);

    expect(user.name).toBe("Alice ๐Ÿ‘ฉ");
    expect(mockRepo.save).toHaveBeenCalledWith(user);
  });
});

๐Ÿ—๏ธ Testing Async Operations

Handle promises and async code properly:

// ๐Ÿš€ Async function to test
class ApiClient {
  async fetchUserData(id: string): Promise<UserData> {
    const response = await fetch(`/api/users/${id}`);
    if (!response.ok) {
      throw new Error(`๐Ÿšซ User ${id} not found`);
    }
    return response.json();
  }
}

// ๐Ÿงช Test async operations
describe("ApiClient", () => {
  it("should fetch user data successfully", async () => {
    // ๐ŸŽญ Mock fetch
    global.fetch = jest.fn().mockResolvedValue({
      ok: true,
      json: async () => ({ name: "Alice ๐Ÿ‘ฉ", id: "123" })
    });

    const client = new ApiClient();
    const userData = await client.fetchUserData("123");

    expect(userData.name).toBe("Alice ๐Ÿ‘ฉ");
    expect(fetch).toHaveBeenCalledWith("/api/users/123");
  });

  it("should handle errors properly", async () => {
    // ๐ŸŽญ Mock failed response
    global.fetch = jest.fn().mockResolvedValue({
      ok: false
    });

    const client = new ApiClient();

    await expect(client.fetchUserData("999"))
      .rejects
      .toThrow("๐Ÿšซ User 999 not found");
  });
});

โš ๏ธ Common Pitfalls and Solutions

๐Ÿ˜ฑ Pitfall 1: Testing Implementation Instead of Behavior

// โŒ Wrong way - testing implementation details!
it("should call the internal method", () => {
  const calculator = new Calculator();
  const spy = jest.spyOn(calculator, 'internalCalculation');
  
  calculator.add(2, 3);
  
  expect(spy).toHaveBeenCalled(); // ๐Ÿ’ฅ Brittle test!
});

// โœ… Correct way - test behavior!
it("should return correct sum", () => {
  const calculator = new Calculator();
  
  const result = calculator.add(2, 3);
  
  expect(result).toBe(5); // โœ… Tests what matters!
});

๐Ÿคฏ Pitfall 2: Overly Complex Tests

// โŒ Complex test that's hard to understand
it("should handle complex scenario", () => {
  const cart = new ShoppingCart();
  const items = [
    { id: "1", name: "Item 1", price: 10 },
    { id: "2", name: "Item 2", price: 20 },
    { id: "3", name: "Item 3", price: 30 }
  ];
  items.forEach(item => cart.addItem(item));
  cart.removeItem("2");
  cart.applyDiscount(0.1);
  expect(cart.getTotal()).toBe(36); // ๐Ÿค” How did we get 36?
});

// โœ… Simple, focused tests
describe("ShoppingCart discounts", () => {
  it("should apply 10% discount correctly", () => {
    const cart = new ShoppingCart();
    const item = { id: "1", name: "Book ๐Ÿ“˜", price: 100 };
    
    cart.addItem(item);
    cart.applyDiscount(0.1);
    
    expect(cart.getTotal()).toBe(90); // โœ… Clear expectation!
  });
});

๐Ÿ› ๏ธ Best Practices

  1. ๐ŸŽฏ One Assertion Per Test: Keep tests focused and clear
  2. ๐Ÿ“ Descriptive Test Names: โ€œshould calculate total with discount appliedโ€
  3. ๐Ÿ›ก๏ธ Arrange-Act-Assert: Structure your tests consistently
  4. ๐ŸŽจ Use TypeScript Types: Make your tests type-safe too
  5. โœจ Keep Tests Simple: If a test is complex, break it down

๐Ÿงช Hands-On Exercise

๐ŸŽฏ Challenge: Build a TDD Task Manager

Create a task management system using TDD:

๐Ÿ“‹ Requirements:

  • โœ… Create, complete, and delete tasks
  • ๐Ÿท๏ธ Add categories and priorities
  • ๐Ÿ“… Set due dates with validation
  • ๐Ÿ“Š Generate completion statistics
  • ๐ŸŽจ Each task needs an emoji!

๐Ÿš€ Bonus Points:

  • Add task assignment to users
  • Implement filtering and sorting
  • Create notification system for overdue tasks

๐Ÿ’ก Solution

๐Ÿ” Click to see solution
// ๐Ÿ”ด RED: Start with comprehensive tests
describe("TaskManager", () => {
  describe("Task Creation", () => {
    it("should create a task with required fields", () => {
      const manager = new TaskManager();
      const taskData = {
        title: "Learn TDD ๐Ÿงช",
        category: "learning" as const,
        priority: "high" as const
      };

      const task = manager.createTask(taskData);

      expect(task.title).toBe("Learn TDD ๐Ÿงช");
      expect(task.category).toBe("learning");
      expect(task.priority).toBe("high");
      expect(task.completed).toBe(false);
      expect(task.id).toBeDefined();
    });

    it("should validate due dates", () => {
      const manager = new TaskManager();
      const yesterday = new Date();
      yesterday.setDate(yesterday.getDate() - 1);

      expect(() => {
        manager.createTask({
          title: "Overdue task ๐Ÿ˜…",
          category: "work",
          priority: "low",
          dueDate: yesterday
        });
      }).toThrow("๐Ÿšซ Due date cannot be in the past");
    });
  });

  describe("Task Management", () => {
    it("should complete tasks", () => {
      const manager = new TaskManager();
      const task = manager.createTask({
        title: "Complete this task โœ…",
        category: "work",
        priority: "medium"
      });

      manager.completeTask(task.id);

      expect(manager.getTask(task.id)?.completed).toBe(true);
    });

    it("should filter tasks by category", () => {
      const manager = new TaskManager();
      manager.createTask({ title: "Work task ๐Ÿ’ผ", category: "work", priority: "high" });
      manager.createTask({ title: "Personal task ๐Ÿ ", category: "personal", priority: "low" });

      const workTasks = manager.getTasksByCategory("work");

      expect(workTasks).toHaveLength(1);
      expect(workTasks[0].title).toBe("Work task ๐Ÿ’ผ");
    });
  });

  describe("Statistics", () => {
    it("should calculate completion percentage", () => {
      const manager = new TaskManager();
      const task1 = manager.createTask({ title: "Task 1 ๐Ÿ“", category: "work", priority: "high" });
      const task2 = manager.createTask({ title: "Task 2 ๐Ÿ“", category: "work", priority: "high" });

      manager.completeTask(task1.id);

      expect(manager.getCompletionPercentage()).toBe(50);
    });

    it("should generate category statistics", () => {
      const manager = new TaskManager();
      manager.createTask({ title: "Work 1 ๐Ÿ’ผ", category: "work", priority: "high" });
      manager.createTask({ title: "Work 2 ๐Ÿ’ผ", category: "work", priority: "low" });
      manager.createTask({ title: "Personal ๐Ÿ ", category: "personal", priority: "medium" });

      const stats = manager.getCategoryStats();

      expect(stats.work).toBe(2);
      expect(stats.personal).toBe(1);
    });
  });
});

// ๐ŸŸข GREEN: Implement the functionality
type Priority = "low" | "medium" | "high";
type Category = "work" | "personal" | "learning" | "health";

interface Task {
  id: string;
  title: string;
  category: Category;
  priority: Priority;
  completed: boolean;
  createdAt: Date;
  dueDate?: Date;
  completedAt?: Date;
}

interface CreateTaskData {
  title: string;
  category: Category;
  priority: Priority;
  dueDate?: Date;
}

class TaskManager {
  private tasks: Map<string, Task> = new Map();
  private nextId = 1;

  createTask(data: CreateTaskData): Task {
    // ๐Ÿ›ก๏ธ Validate due date
    if (data.dueDate && data.dueDate < new Date()) {
      throw new Error("๐Ÿšซ Due date cannot be in the past");
    }

    const task: Task = {
      id: this.nextId.toString(),
      title: data.title,
      category: data.category,
      priority: data.priority,
      completed: false,
      createdAt: new Date(),
      dueDate: data.dueDate
    };

    this.tasks.set(task.id, task);
    this.nextId++;

    return task;
  }

  getTask(id: string): Task | undefined {
    return this.tasks.get(id);
  }

  completeTask(id: string): void {
    const task = this.tasks.get(id);
    if (!task) throw new Error(`๐Ÿšซ Task ${id} not found`);

    task.completed = true;
    task.completedAt = new Date();
  }

  deleteTask(id: string): void {
    if (!this.tasks.has(id)) {
      throw new Error(`๐Ÿšซ Task ${id} not found`);
    }
    this.tasks.delete(id);
  }

  getTasksByCategory(category: Category): Task[] {
    return Array.from(this.tasks.values())
      .filter(task => task.category === category);
  }

  getCompletionPercentage(): number {
    const totalTasks = this.tasks.size;
    if (totalTasks === 0) return 100;

    const completedTasks = Array.from(this.tasks.values())
      .filter(task => task.completed).length;

    return Math.round((completedTasks / totalTasks) * 100);
  }

  getCategoryStats(): Record<Category, number> {
    const stats: Record<Category, number> = {
      work: 0,
      personal: 0,
      learning: 0,
      health: 0
    };

    for (const task of this.tasks.values()) {
      stats[task.category]++;
    }

    return stats;
  }

  // ๐Ÿ”ต REFACTOR: Add priority sorting
  getTasksByPriority(): Task[] {
    const priorityOrder = { high: 3, medium: 2, low: 1 };
    
    return Array.from(this.tasks.values())
      .sort((a, b) => priorityOrder[b.priority] - priorityOrder[a.priority]);
  }

  // ๐ŸŽฏ Get overdue tasks
  getOverdueTasks(): Task[] {
    const now = new Date();
    return Array.from(this.tasks.values())
      .filter(task => 
        !task.completed && 
        task.dueDate && 
        task.dueDate < now
      );
  }
}

// ๐ŸŽฎ Usage example
const taskManager = new TaskManager();

// ๐Ÿ“ Create some tasks
const learningTask = taskManager.createTask({
  title: "Master TDD ๐Ÿงช",
  category: "learning",
  priority: "high",
  dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000) // 1 week from now
});

const workTask = taskManager.createTask({
  title: "Deploy new feature ๐Ÿš€",
  category: "work",
  priority: "high"
});

// โœ… Complete a task
taskManager.completeTask(learningTask.id);

// ๐Ÿ“Š Check progress
console.log(`๐Ÿ“ˆ Completion: ${taskManager.getCompletionPercentage()}%`);
console.log("๐Ÿ“‹ High priority tasks:", taskManager.getTasksByPriority().slice(0, 3));

๐ŸŽ“ Key Takeaways

Youโ€™ve learned so much about TDD! Hereโ€™s what you can now do:

  • โœ… Apply the Red-Green-Refactor cycle with confidence ๐Ÿ’ช
  • โœ… Write focused, maintainable tests that guide your development ๐Ÿ›ก๏ธ
  • โœ… Mock dependencies and test async code like a pro ๐ŸŽฏ
  • โœ… Avoid common TDD pitfalls and write better tests ๐Ÿ›
  • โœ… Build type-safe, well-tested TypeScript applications ๐Ÿš€

Remember: TDD isnโ€™t just about testing - itโ€™s about designing better software! It helps you think before you code. ๐Ÿค

๐Ÿค Next Steps

Congratulations! ๐ŸŽ‰ Youโ€™ve mastered Test-Driven Development with TypeScript!

Hereโ€™s what to do next:

  1. ๐Ÿ’ป Practice TDD with the exercise above
  2. ๐Ÿ—๏ธ Start your next project using TDD from day one
  3. ๐Ÿ“š Explore advanced testing patterns like BDD and property-based testing
  4. ๐ŸŒŸ Share your TDD journey with the community!

Remember: Every expert was once a beginner who refused to give up. Keep writing tests, keep refactoring, and most importantly, have fun building reliable software! ๐Ÿš€


Happy testing! ๐ŸŽ‰๐Ÿงชโœจ