+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 144 of 355

🎭 Mocking in TypeScript: Type-Safe Mocks

Master the art of creating type-safe mocks in TypeScript, enabling isolated testing with full type checking and IntelliSense support 🚀

💎Advanced
28 min read

Prerequisites

  • Understanding of TypeScript interfaces and classes 📝
  • Knowledge of unit testing fundamentals and Jest ⚡
  • Familiarity with dependency injection patterns 💻

What you'll learn

  • Create sophisticated type-safe mocks for TypeScript interfaces and classes 🎯
  • Master advanced mocking patterns and mock management strategies 🏗️
  • Build comprehensive mock systems for complex applications 🐛
  • Leverage TypeScript's type system for better test reliability ✨

🎯 Introduction

Welcome to the theater of TypeScript testing, where mocks are the actors that play the roles of real objects! 🎭 If testing were like producing a play, then mocking would be like having incredibly talented understudies who can perfectly imitate any actor, deliver their lines flawlessly, and even help you rehearse scenes without needing the real stars to be present - except in our case, these “understudies” are type-safe mock objects that help us test our code in isolation!

Mocking in TypeScript combines the power of traditional mocking with the safety and developer experience of static typing. You get all the benefits of isolated testing while maintaining IntelliSense, compile-time checking, and clear contracts between your tests and the code they’re testing.

By the end of this tutorial, you’ll be a master of TypeScript mocking, capable of creating sophisticated mock systems that are type-safe, maintainable, and provide excellent developer experience. You’ll learn to mock everything from simple functions to complex class hierarchies while leveraging TypeScript’s type system to catch errors early and make your tests more reliable. Let’s dive into the world of type-safe testing! 🌟

📚 Understanding Type-Safe Mocking

🤔 Why Type-Safe Mocks Matter

Type-safe mocks provide compile-time checking, better IntelliSense, and help catch interface changes during refactoring.

// 🌟 Setting up comprehensive type-safe mocking

interface DatabaseConnection {
  connect(): Promise<void>;
  disconnect(): Promise<void>;
  query<T>(sql: string, params?: any[]): Promise<T[]>;
  transaction<T>(callback: (tx: Transaction) => Promise<T>): Promise<T>;
  isConnected(): boolean;
}

interface Transaction {
  query<T>(sql: string, params?: any[]): Promise<T[]>;
  commit(): Promise<void>;
  rollback(): Promise<void>;
}

interface EmailService {
  sendEmail(to: string, subject: string, body: string): Promise<void>;
  sendTemplatedEmail(to: string, templateId: string, data: any): Promise<void>;
  verifyEmail(email: string): Promise<boolean>;
}

interface Logger {
  debug(message: string, meta?: any): void;
  info(message: string, meta?: any): void;
  warn(message: string, meta?: any): void;
  error(message: string, error?: Error, meta?: any): void;
}

interface User {
  id: string;
  email: string;
  name: string;
  isActive: boolean;
  createdAt: Date;
}

interface CreateUserData {
  email: string;
  name: string;
}

// Service that depends on multiple interfaces
class UserService {
  constructor(
    private db: DatabaseConnection,
    private emailService: EmailService,
    private logger: Logger
  ) {}
  
  async createUser(userData: CreateUserData): Promise<User> {
    this.logger.info('Creating new user', { email: userData.email });
    
    try {
      // Check if user already exists
      const existingUsers = await this.db.query<User>(
        'SELECT * FROM users WHERE email = ?',
        [userData.email]
      );
      
      if (existingUsers.length > 0) {
        throw new Error('User with this email already exists');
      }
      
      // Create user in transaction
      const user = await this.db.transaction(async (tx) => {
        const result = await tx.query<{ insertId: string }>(
          'INSERT INTO users (email, name, is_active, created_at) VALUES (?, ?, ?, ?)',
          [userData.email, userData.name, true, new Date()]
        );
        
        const newUser: User = {
          id: result[0].insertId,
          email: userData.email,
          name: userData.name,
          isActive: true,
          createdAt: new Date()
        };
        
        return newUser;
      });
      
      // Send welcome email
      await this.emailService.sendTemplatedEmail(
        user.email,
        'welcome',
        { name: user.name }
      );
      
      this.logger.info('User created successfully', { userId: user.id });
      return user;
      
    } catch (error) {
      this.logger.error('Failed to create user', error as Error, { email: userData.email });
      throw error;
    }
  }
  
  async getUserById(id: string): Promise<User | null> {
    this.logger.debug('Fetching user by ID', { userId: id });
    
    const users = await this.db.query<User>(
      'SELECT * FROM users WHERE id = ?',
      [id]
    );
    
    return users[0] || null;
  }
  
  async updateUser(id: string, updates: Partial<User>): Promise<User | null> {
    this.logger.info('Updating user', { userId: id, updates });
    
    const existingUser = await this.getUserById(id);
    if (!existingUser) {
      return null;
    }
    
    const updatedUser = await this.db.transaction(async (tx) => {
      await tx.query(
        'UPDATE users SET email = ?, name = ?, is_active = ? WHERE id = ?',
        [
          updates.email || existingUser.email,
          updates.name || existingUser.name,
          updates.isActive !== undefined ? updates.isActive : existingUser.isActive,
          id
        ]
      );
      
      return {
        ...existingUser,
        ...updates
      };
    });
    
    this.logger.info('User updated successfully', { userId: id });
    return updatedUser;
  }
  
  async deleteUser(id: string): Promise<boolean> {
    this.logger.info('Deleting user', { userId: id });
    
    const result = await this.db.query(
      'DELETE FROM users WHERE id = ?',
      [id]
    );
    
    const deleted = (result as any).affectedRows > 0;
    
    if (deleted) {
      this.logger.info('User deleted successfully', { userId: id });
    } else {
      this.logger.warn('User not found for deletion', { userId: id });
    }
    
    return deleted;
  }
}

// 🧪 Type-safe mocking with Jest
describe('UserService', () => {
  let userService: UserService;
  let mockDb: jest.Mocked<DatabaseConnection>;
  let mockEmailService: jest.Mocked<EmailService>;
  let mockLogger: jest.Mocked<Logger>;
  let mockTransaction: jest.Mocked<Transaction>;
  
  beforeEach(() => {
    // Create type-safe mocks
    mockTransaction = {
      query: jest.fn(),
      commit: jest.fn(),
      rollback: jest.fn()
    };
    
    mockDb = {
      connect: jest.fn(),
      disconnect: jest.fn(),
      query: jest.fn(),
      transaction: jest.fn(),
      isConnected: jest.fn()
    };
    
    mockEmailService = {
      sendEmail: jest.fn(),
      sendTemplatedEmail: jest.fn(),
      verifyEmail: jest.fn()
    };
    
    mockLogger = {
      debug: jest.fn(),
      info: jest.fn(),
      warn: jest.fn(),
      error: jest.fn()
    };
    
    userService = new UserService(mockDb, mockEmailService, mockLogger);
  });
  
  describe('createUser', () => {
    const userData: CreateUserData = {
      email: '[email protected]',
      name: 'John Doe'
    };
    
    it('should create user successfully when email does not exist', async () => {
      // Setup mocks with type safety
      mockDb.query.mockResolvedValueOnce([]); // No existing users
      mockDb.transaction.mockImplementation(async (callback) => {
        // Mock transaction behavior
        const mockTxResult = [{ insertId: 'user_123' }];
        mockTransaction.query.mockResolvedValueOnce(mockTxResult);
        return callback(mockTransaction);
      });
      mockEmailService.sendTemplatedEmail.mockResolvedValueOnce();
      
      // Execute
      const result = await userService.createUser(userData);
      
      // Verify
      expect(result).toMatchObject({
        id: 'user_123',
        email: '[email protected]',
        name: 'John Doe',
        isActive: true
      });
      expect(result.createdAt).toBeInstanceOf(Date);
      
      // Verify interactions
      expect(mockDb.query).toHaveBeenCalledWith(
        'SELECT * FROM users WHERE email = ?',
        ['[email protected]']
      );
      
      expect(mockDb.transaction).toHaveBeenCalledWith(expect.any(Function));
      
      expect(mockEmailService.sendTemplatedEmail).toHaveBeenCalledWith(
        '[email protected]',
        'welcome',
        { name: 'John Doe' }
      );
      
      expect(mockLogger.info).toHaveBeenCalledWith(
        'Creating new user',
        { email: '[email protected]' }
      );
      
      expect(mockLogger.info).toHaveBeenCalledWith(
        'User created successfully',
        { userId: 'user_123' }
      );
    });
    
    it('should throw error when user with email already exists', async () => {
      // Setup mock to return existing user
      const existingUser: User = {
        id: 'existing_id',
        email: '[email protected]',
        name: 'Existing John',
        isActive: true,
        createdAt: new Date()
      };
      
      mockDb.query.mockResolvedValueOnce([existingUser]);
      
      // Execute and verify
      await expect(userService.createUser(userData)).rejects.toThrow(
        'User with this email already exists'
      );
      
      // Verify transaction was not called
      expect(mockDb.transaction).not.toHaveBeenCalled();
      expect(mockEmailService.sendTemplatedEmail).not.toHaveBeenCalled();
      
      // Verify error logging
      expect(mockLogger.error).toHaveBeenCalledWith(
        'Failed to create user',
        expect.any(Error),
        { email: '[email protected]' }
      );
    });
    
    it('should handle database errors during user creation', async () => {
      mockDb.query.mockResolvedValueOnce([]); // No existing users
      mockDb.transaction.mockRejectedValueOnce(new Error('Database connection failed'));
      
      await expect(userService.createUser(userData)).rejects.toThrow(
        'Database connection failed'
      );
      
      expect(mockEmailService.sendTemplatedEmail).not.toHaveBeenCalled();
      expect(mockLogger.error).toHaveBeenCalledWith(
        'Failed to create user',
        expect.any(Error),
        { email: '[email protected]' }
      );
    });
    
    it('should handle email service errors', async () => {
      mockDb.query.mockResolvedValueOnce([]);
      mockDb.transaction.mockImplementation(async (callback) => {
        mockTransaction.query.mockResolvedValueOnce([{ insertId: 'user_123' }]);
        return callback(mockTransaction);
      });
      mockEmailService.sendTemplatedEmail.mockRejectedValueOnce(
        new Error('Email service unavailable')
      );
      
      await expect(userService.createUser(userData)).rejects.toThrow(
        'Email service unavailable'
      );
      
      expect(mockLogger.error).toHaveBeenCalledWith(
        'Failed to create user',
        expect.any(Error),
        { email: '[email protected]' }
      );
    });
  });
  
  describe('getUserById', () => {
    it('should return user when found', async () => {
      const mockUser: User = {
        id: 'user_123',
        email: '[email protected]',
        name: 'John Doe',
        isActive: true,
        createdAt: new Date()
      };
      
      mockDb.query.mockResolvedValueOnce([mockUser]);
      
      const result = await userService.getUserById('user_123');
      
      expect(result).toEqual(mockUser);
      expect(mockDb.query).toHaveBeenCalledWith(
        'SELECT * FROM users WHERE id = ?',
        ['user_123']
      );
      expect(mockLogger.debug).toHaveBeenCalledWith(
        'Fetching user by ID',
        { userId: 'user_123' }
      );
    });
    
    it('should return null when user not found', async () => {
      mockDb.query.mockResolvedValueOnce([]);
      
      const result = await userService.getUserById('nonexistent');
      
      expect(result).toBeNull();
    });
  });
  
  describe('updateUser', () => {
    const existingUser: User = {
      id: 'user_123',
      email: '[email protected]',
      name: 'John Doe',
      isActive: true,
      createdAt: new Date()
    };
    
    it('should update user successfully', async () => {
      const updates = { name: 'John Smith', isActive: false };
      
      // Mock getUserById call
      mockDb.query.mockResolvedValueOnce([existingUser]);
      
      // Mock transaction
      mockDb.transaction.mockImplementation(async (callback) => {
        return callback(mockTransaction);
      });
      
      const result = await userService.updateUser('user_123', updates);
      
      expect(result).toMatchObject({
        ...existingUser,
        ...updates
      });
      
      expect(mockTransaction.query).toHaveBeenCalledWith(
        'UPDATE users SET email = ?, name = ?, is_active = ? WHERE id = ?',
        ['[email protected]', 'John Smith', false, 'user_123']
      );
    });
    
    it('should return null when user not found', async () => {
      mockDb.query.mockResolvedValueOnce([]); // No user found
      
      const result = await userService.updateUser('nonexistent', { name: 'New Name' });
      
      expect(result).toBeNull();
      expect(mockDb.transaction).not.toHaveBeenCalled();
    });
  });
  
  describe('deleteUser', () => {
    it('should delete user successfully when user exists', async () => {
      mockDb.query.mockResolvedValueOnce({ affectedRows: 1 } as any);
      
      const result = await userService.deleteUser('user_123');
      
      expect(result).toBe(true);
      expect(mockDb.query).toHaveBeenCalledWith(
        'DELETE FROM users WHERE id = ?',
        ['user_123']
      );
      expect(mockLogger.info).toHaveBeenCalledWith(
        'User deleted successfully',
        { userId: 'user_123' }
      );
    });
    
    it('should return false when user does not exist', async () => {
      mockDb.query.mockResolvedValueOnce({ affectedRows: 0 } as any);
      
      const result = await userService.deleteUser('nonexistent');
      
      expect(result).toBe(false);
      expect(mockLogger.warn).toHaveBeenCalledWith(
        'User not found for deletion',
        { userId: 'nonexistent' }
      );
    });
  });
});

🏗️ Advanced Mocking Patterns

// 🚀 Creating sophisticated mock factories and utilities

// Mock factory for creating consistent test data
class MockFactory {
  private static idCounter = 1;
  
  static createUser(overrides: Partial<User> = {}): User {
    const id = `user_${MockFactory.idCounter++}`;
    return {
      id,
      email: `user${MockFactory.idCounter}@example.com`,
      name: `User ${MockFactory.idCounter}`,
      isActive: true,
      createdAt: new Date(),
      ...overrides
    };
  }
  
  static createMockDatabase(): jest.Mocked<DatabaseConnection> {
    return {
      connect: jest.fn().mockResolvedValue(undefined),
      disconnect: jest.fn().mockResolvedValue(undefined),
      query: jest.fn(),
      transaction: jest.fn(),
      isConnected: jest.fn().mockReturnValue(true)
    };
  }
  
  static createMockEmailService(): jest.Mocked<EmailService> {
    return {
      sendEmail: jest.fn().mockResolvedValue(undefined),
      sendTemplatedEmail: jest.fn().mockResolvedValue(undefined),
      verifyEmail: jest.fn().mockResolvedValue(true)
    };
  }
  
  static createMockLogger(): jest.Mocked<Logger> {
    return {
      debug: jest.fn(),
      info: jest.fn(),
      warn: jest.fn(),
      error: jest.fn()
    };
  }
  
  static reset(): void {
    MockFactory.idCounter = 1;
  }
}

// Advanced mock builder with fluent interface
class DatabaseMockBuilder {
  private mockDb: jest.Mocked<DatabaseConnection>;
  
  constructor() {
    this.mockDb = MockFactory.createMockDatabase();
  }
  
  withSuccessfulConnection(): this {
    this.mockDb.connect.mockResolvedValue(undefined);
    this.mockDb.isConnected.mockReturnValue(true);
    return this;
  }
  
  withConnectionFailure(error: Error): this {
    this.mockDb.connect.mockRejectedValue(error);
    this.mockDb.isConnected.mockReturnValue(false);
    return this;
  }
  
  withQueryResult<T>(sql: string, result: T[]): this {
    this.mockDb.query.mockImplementation((querySql: string) => {
      if (querySql.includes(sql)) {
        return Promise.resolve(result);
      }
      return Promise.resolve([]);
    });
    return this;
  }
  
  withQueryError(sql: string, error: Error): this {
    this.mockDb.query.mockImplementation((querySql: string) => {
      if (querySql.includes(sql)) {
        return Promise.reject(error);
      }
      return Promise.resolve([]);
    });
    return this;
  }
  
  withSuccessfulTransaction(): this {
    this.mockDb.transaction.mockImplementation(async (callback) => {
      const mockTx: jest.Mocked<Transaction> = {
        query: jest.fn().mockResolvedValue([]),
        commit: jest.fn().mockResolvedValue(undefined),
        rollback: jest.fn().mockResolvedValue(undefined)
      };
      return callback(mockTx);
    });
    return this;
  }
  
  withTransactionFailure(error: Error): this {
    this.mockDb.transaction.mockRejectedValue(error);
    return this;
  }
  
  build(): jest.Mocked<DatabaseConnection> {
    return this.mockDb;
  }
}

// Behavior-driven mock setup
class EmailServiceMockBuilder {
  private mockEmailService: jest.Mocked<EmailService>;
  
  constructor() {
    this.mockEmailService = MockFactory.createMockEmailService();
  }
  
  withSuccessfulEmailSending(): this {
    this.mockEmailService.sendEmail.mockResolvedValue(undefined);
    this.mockEmailService.sendTemplatedEmail.mockResolvedValue(undefined);
    return this;
  }
  
  withEmailSendingFailure(error: Error): this {
    this.mockEmailService.sendEmail.mockRejectedValue(error);
    this.mockEmailService.sendTemplatedEmail.mockRejectedValue(error);
    return this;
  }
  
  withTemplateEmailFailure(templateId: string, error: Error): this {
    this.mockEmailService.sendTemplatedEmail.mockImplementation(
      (to: string, template: string, data: any) => {
        if (template === templateId) {
          return Promise.reject(error);
        }
        return Promise.resolve();
      }
    );
    return this;
  }
  
  withEmailVerification(email: string, isValid: boolean): this {
    this.mockEmailService.verifyEmail.mockImplementation((emailToVerify: string) => {
      if (emailToVerify === email) {
        return Promise.resolve(isValid);
      }
      return Promise.resolve(true);
    });
    return this;
  }
  
  build(): jest.Mocked<EmailService> {
    return this.mockEmailService;
  }
}

// Testing with builder pattern
describe('UserService with Mock Builders', () => {
  let userService: UserService;
  
  beforeEach(() => {
    MockFactory.reset();
  });
  
  it('should handle complex scenarios with mock builders', async () => {
    // Setup sophisticated mocks using builders
    const mockDb = new DatabaseMockBuilder()
      .withSuccessfulConnection()
      .withQueryResult('SELECT * FROM users WHERE email', []) // No existing users
      .withSuccessfulTransaction()
      .build();
    
    const mockEmailService = new EmailServiceMockBuilder()
      .withSuccessfulEmailSending()
      .build();
    
    const mockLogger = MockFactory.createMockLogger();
    
    userService = new UserService(mockDb, mockEmailService, mockLogger);
    
    // Setup transaction behavior
    mockDb.transaction.mockImplementation(async (callback) => {
      const mockTx: jest.Mocked<Transaction> = {
        query: jest.fn().mockResolvedValue([{ insertId: 'user_456' }]),
        commit: jest.fn(),
        rollback: jest.fn()
      };
      return callback(mockTx);
    });
    
    const userData: CreateUserData = {
      email: '[email protected]',
      name: 'Test User'
    };
    
    const result = await userService.createUser(userData);
    
    expect(result.id).toBe('user_456');
    expect(result.email).toBe('[email protected]');
    expect(mockEmailService.sendTemplatedEmail).toHaveBeenCalledWith(
      '[email protected]',
      'welcome',
      { name: 'Test User' }
    );
  });
  
  it('should handle email service failures gracefully', async () => {
    const mockDb = new DatabaseMockBuilder()
      .withSuccessfulConnection()
      .withQueryResult('SELECT * FROM users WHERE email', [])
      .withSuccessfulTransaction()
      .build();
    
    const mockEmailService = new EmailServiceMockBuilder()
      .withTemplateEmailFailure('welcome', new Error('Template not found'))
      .build();
    
    const mockLogger = MockFactory.createMockLogger();
    
    userService = new UserService(mockDb, mockEmailService, mockLogger);
    
    // Setup transaction to succeed
    mockDb.transaction.mockImplementation(async (callback) => {
      const mockTx: jest.Mocked<Transaction> = {
        query: jest.fn().mockResolvedValue([{ insertId: 'user_789' }]),
        commit: jest.fn(),
        rollback: jest.fn()
      };
      return callback(mockTx);
    });
    
    const userData: CreateUserData = {
      email: '[email protected]',
      name: 'Failing User'
    };
    
    await expect(userService.createUser(userData)).rejects.toThrow('Template not found');
    
    expect(mockLogger.error).toHaveBeenCalledWith(
      'Failed to create user',
      expect.any(Error),
      { email: '[email protected]' }
    );
  });
});

// 🎯 Mocking Complex Class Hierarchies
abstract class PaymentProcessor {
  abstract processPayment(amount: number, currency: string): Promise<PaymentResult>;
  abstract refundPayment(transactionId: string): Promise<RefundResult>;
  
  protected validateAmount(amount: number): void {
    if (amount <= 0) {
      throw new Error('Amount must be positive');
    }
  }
  
  protected generateTransactionId(): string {
    return `txn_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
  }
}

interface PaymentResult {
  success: boolean;
  transactionId: string;
  amount: number;
  currency: string;
  processingFee: number;
  timestamp: Date;
}

interface RefundResult {
  success: boolean;
  refundId: string;
  originalTransactionId: string;
  amount: number;
  timestamp: Date;
}

class StripePaymentProcessor extends PaymentProcessor {
  constructor(private apiKey: string, private webhookSecret: string) {
    super();
  }
  
  async processPayment(amount: number, currency: string): Promise<PaymentResult> {
    this.validateAmount(amount);
    
    // Simulate Stripe API call
    const transactionId = this.generateTransactionId();
    const processingFee = Math.round(amount * 0.029 + 30); // Stripe's fee structure
    
    return {
      success: true,
      transactionId,
      amount,
      currency,
      processingFee,
      timestamp: new Date()
    };
  }
  
  async refundPayment(transactionId: string): Promise<RefundResult> {
    // Simulate Stripe refund API call
    return {
      success: true,
      refundId: `ref_${Date.now()}`,
      originalTransactionId: transactionId,
      amount: 1000, // Would come from Stripe API
      timestamp: new Date()
    };
  }
  
  async setupWebhook(endpoint: string): Promise<void> {
    // Webhook setup logic
  }
  
  validateWebhookSignature(payload: string, signature: string): boolean {
    // Signature validation logic
    return true;
  }
}

class PaymentService {
  constructor(
    private processor: PaymentProcessor,
    private logger: Logger,
    private db: DatabaseConnection
  ) {}
  
  async processPayment(
    userId: string,
    amount: number,
    currency: string
  ): Promise<PaymentResult> {
    this.logger.info('Processing payment', { userId, amount, currency });
    
    try {
      const result = await this.processor.processPayment(amount, currency);
      
      // Save to database
      await this.db.query(
        'INSERT INTO payments (user_id, transaction_id, amount, currency, status) VALUES (?, ?, ?, ?, ?)',
        [userId, result.transactionId, amount, currency, 'completed']
      );
      
      this.logger.info('Payment processed successfully', {
        userId,
        transactionId: result.transactionId
      });
      
      return result;
    } catch (error) {
      this.logger.error('Payment processing failed', error as Error, { userId, amount });
      throw error;
    }
  }
  
  async refundPayment(transactionId: string): Promise<RefundResult> {
    this.logger.info('Processing refund', { transactionId });
    
    try {
      const result = await this.processor.refundPayment(transactionId);
      
      // Update database
      await this.db.query(
        'UPDATE payments SET status = ? WHERE transaction_id = ?',
        ['refunded', transactionId]
      );
      
      this.logger.info('Refund processed successfully', {
        transactionId,
        refundId: result.refundId
      });
      
      return result;
    } catch (error) {
      this.logger.error('Refund processing failed', error as Error, { transactionId });
      throw error;
    }
  }
}

// Mocking abstract classes and inheritance
describe('PaymentService', () => {
  let paymentService: PaymentService;
  let mockProcessor: jest.Mocked<PaymentProcessor>;
  let mockLogger: jest.Mocked<Logger>;
  let mockDb: jest.Mocked<DatabaseConnection>;
  
  beforeEach(() => {
    // Create mock for abstract class
    mockProcessor = {
      processPayment: jest.fn(),
      refundPayment: jest.fn()
    } as jest.Mocked<PaymentProcessor>;
    
    mockLogger = MockFactory.createMockLogger();
    mockDb = MockFactory.createMockDatabase();
    
    paymentService = new PaymentService(mockProcessor, mockLogger, mockDb);
  });
  
  describe('processPayment', () => {
    it('should process payment successfully', async () => {
      const mockResult: PaymentResult = {
        success: true,
        transactionId: 'txn_123',
        amount: 1000,
        currency: 'USD',
        processingFee: 59,
        timestamp: new Date()
      };
      
      mockProcessor.processPayment.mockResolvedValue(mockResult);
      mockDb.query.mockResolvedValue([]);
      
      const result = await paymentService.processPayment('user_456', 1000, 'USD');
      
      expect(result).toEqual(mockResult);
      expect(mockProcessor.processPayment).toHaveBeenCalledWith(1000, 'USD');
      expect(mockDb.query).toHaveBeenCalledWith(
        'INSERT INTO payments (user_id, transaction_id, amount, currency, status) VALUES (?, ?, ?, ?, ?)',
        ['user_456', 'txn_123', 1000, 'USD', 'completed']
      );
      expect(mockLogger.info).toHaveBeenCalledWith(
        'Payment processed successfully',
        { userId: 'user_456', transactionId: 'txn_123' }
      );
    });
    
    it('should handle payment processor errors', async () => {
      const error = new Error('Payment failed');
      mockProcessor.processPayment.mockRejectedValue(error);
      
      await expect(paymentService.processPayment('user_456', 1000, 'USD'))
        .rejects.toThrow('Payment failed');
      
      expect(mockDb.query).not.toHaveBeenCalled();
      expect(mockLogger.error).toHaveBeenCalledWith(
        'Payment processing failed',
        error,
        { userId: 'user_456', amount: 1000 }
      );
    });
    
    it('should handle database errors', async () => {
      const mockResult: PaymentResult = {
        success: true,
        transactionId: 'txn_123',
        amount: 1000,
        currency: 'USD',
        processingFee: 59,
        timestamp: new Date()
      };
      
      mockProcessor.processPayment.mockResolvedValue(mockResult);
      mockDb.query.mockRejectedValue(new Error('Database error'));
      
      await expect(paymentService.processPayment('user_456', 1000, 'USD'))
        .rejects.toThrow('Database error');
    });
  });
  
  describe('refundPayment', () => {
    it('should process refund successfully', async () => {
      const mockResult: RefundResult = {
        success: true,
        refundId: 'ref_789',
        originalTransactionId: 'txn_123',
        amount: 1000,
        timestamp: new Date()
      };
      
      mockProcessor.refundPayment.mockResolvedValue(mockResult);
      mockDb.query.mockResolvedValue([]);
      
      const result = await paymentService.refundPayment('txn_123');
      
      expect(result).toEqual(mockResult);
      expect(mockProcessor.refundPayment).toHaveBeenCalledWith('txn_123');
      expect(mockDb.query).toHaveBeenCalledWith(
        'UPDATE payments SET status = ? WHERE transaction_id = ?',
        ['refunded', 'txn_123']
      );
    });
  });
});

// Testing the concrete implementation with mocks
describe('StripePaymentProcessor', () => {
  let processor: StripePaymentProcessor;
  
  beforeEach(() => {
    processor = new StripePaymentProcessor('test_key', 'test_webhook_secret');
  });
  
  describe('processPayment', () => {
    it('should process payment with correct fee calculation', async () => {
      const result = await processor.processPayment(1000, 'USD');
      
      expect(result.success).toBe(true);
      expect(result.amount).toBe(1000);
      expect(result.currency).toBe('USD');
      expect(result.processingFee).toBe(59); // 2.9% + $0.30
      expect(result.transactionId).toMatch(/^txn_\d+_/);
      expect(result.timestamp).toBeInstanceOf(Date);
    });
    
    it('should throw error for invalid amount', async () => {
      await expect(processor.processPayment(-100, 'USD'))
        .rejects.toThrow('Amount must be positive');
      
      await expect(processor.processPayment(0, 'USD'))
        .rejects.toThrow('Amount must be positive');
    });
  });
  
  describe('refundPayment', () => {
    it('should process refund successfully', async () => {
      const result = await processor.refundPayment('txn_123');
      
      expect(result.success).toBe(true);
      expect(result.originalTransactionId).toBe('txn_123');
      expect(result.refundId).toMatch(/^ref_\d+/);
      expect(result.timestamp).toBeInstanceOf(Date);
    });
  });
});

🛠️ Building a Comprehensive Mock Management System

Let’s create a sophisticated system for managing mocks across large test suites:

// 🏗️ Mock Management System for Large TypeScript Applications

namespace MockManagement {
  
  // 📋 Core interfaces for mock management
  export interface MockRegistry {
    mocks: Map<string, MockDefinition>;
    scenarios: Map<string, MockScenario>;
    templates: Map<string, MockTemplate>;
    metadata: RegistryMetadata;
  }
  
  export interface MockDefinition {
    name: string;
    type: MockType;
    interface: string;
    methods: MockMethod[];
    properties: MockProperty[];
    scenarios: string[];
    defaultBehavior: MockBehavior;
    createdAt: Date;
    lastUsed: Date;
  }
  
  export type MockType = 'interface' | 'class' | 'function' | 'module' | 'global';
  
  export interface MockMethod {
    name: string;
    signature: string;
    returnType: string;
    behaviors: MockBehavior[];
    callCount: number;
    lastCalled?: Date;
  }
  
  export interface MockProperty {
    name: string;
    type: string;
    value: any;
    readonly: boolean;
  }
  
  export interface MockBehavior {
    condition?: MockCondition;
    action: MockAction;
    priority: number;
    description: string;
  }
  
  export interface MockCondition {
    type: 'parameter' | 'call_count' | 'context' | 'custom';
    expression: string;
    value?: any;
  }
  
  export interface MockAction {
    type: 'return' | 'throw' | 'callback' | 'delay' | 'custom';
    value?: any;
    delay?: number;
    callback?: Function;
  }
  
  export interface MockScenario {
    name: string;
    description: string;
    mocks: Map<string, MockBehavior[]>;
    setup: string[];
    teardown: string[];
    tags: string[];
  }
  
  export interface MockTemplate {
    name: string;
    description: string;
    interface: string;
    defaultMethods: MockMethod[];
    defaultProperties: MockProperty[];
    commonScenarios: string[];
  }
  
  export interface RegistryMetadata {
    totalMocks: number;
    totalScenarios: number;
    lastUpdated: Date;
    version: string;
  }
  
  // 🔧 Mock Builder with Advanced Features
  export class AdvancedMockBuilder<T> {
    private mockObject: jest.Mocked<T>;
    private behaviors: Map<string, MockBehavior[]> = new Map();
    private callHistory: Map<string, CallRecord[]> = new Map();
    
    constructor(private template: Partial<T>) {
      this.mockObject = this.createBaseMock();
    }
    
    // 📝 Method behavior configuration
    whenCalled<K extends keyof T>(method: K): MethodBehaviorBuilder<T[K]> {
      return new MethodBehaviorBuilder(
        method as string,
        this.mockObject[method] as jest.Mock,
        this.behaviors
      );
    }
    
    // 🎯 Scenario-based setup
    applyScenario(scenario: MockScenario): this {
      for (const [methodName, behaviors] of scenario.mocks) {
        this.behaviors.set(methodName, behaviors);
        this.applyBehaviors(methodName, behaviors);
      }
      return this;
    }
    
    // 📊 Call tracking and verification
    getCallHistory<K extends keyof T>(method: K): CallRecord[] {
      return this.callHistory.get(method as string) || [];
    }
    
    wasCalledWith<K extends keyof T>(method: K, ...args: any[]): boolean {
      const history = this.getCallHistory(method);
      return history.some(record => 
        record.arguments.length === args.length &&
        record.arguments.every((arg, index) => arg === args[index])
      );
    }
    
    getCallCount<K extends keyof T>(method: K): number {
      return this.getCallHistory(method).length;
    }
    
    // 🔄 Reset and cleanup
    reset(): this {
      this.callHistory.clear();
      this.behaviors.clear();
      jest.clearAllMocks();
      return this;
    }
    
    resetMethod<K extends keyof T>(method: K): this {
      this.callHistory.delete(method as string);
      this.behaviors.delete(method as string);
      (this.mockObject[method] as jest.Mock).mockReset();
      return this;
    }
    
    // 🏗️ Build final mock
    build(): jest.Mocked<T> {
      return this.mockObject;
    }
    
    // 🔧 Private helper methods
    private createBaseMock(): jest.Mocked<T> {
      const mock = {} as jest.Mocked<T>;
      
      for (const key in this.template) {
        if (typeof this.template[key] === 'function') {
          (mock as any)[key] = jest.fn();
          this.setupCallTracking(key as string, (mock as any)[key]);
        } else {
          (mock as any)[key] = this.template[key];
        }
      }
      
      return mock;
    }
    
    private setupCallTracking(methodName: string, mockFn: jest.Mock): void {
      const originalImplementation = mockFn.getMockImplementation();
      
      mockFn.mockImplementation((...args: any[]) => {
        // Record call
        const callRecord: CallRecord = {
          timestamp: new Date(),
          arguments: args,
          stackTrace: new Error().stack || ''
        };
        
        const history = this.callHistory.get(methodName) || [];
        history.push(callRecord);
        this.callHistory.set(methodName, history);
        
        // Execute original behavior
        if (originalImplementation) {
          return originalImplementation(...args);
        }
      });
    }
    
    private applyBehaviors(methodName: string, behaviors: MockBehavior[]): void {
      const mockFn = (this.mockObject as any)[methodName] as jest.Mock;
      
      mockFn.mockImplementation((...args: any[]) => {
        // Find matching behavior
        const matchingBehavior = behaviors
          .filter(b => this.matchesCondition(b.condition, args))
          .sort((a, b) => b.priority - a.priority)[0];
        
        if (matchingBehavior) {
          return this.executeBehavior(matchingBehavior.action, args);
        }
        
        // Default behavior
        return undefined;
      });
    }
    
    private matchesCondition(condition: MockCondition | undefined, args: any[]): boolean {
      if (!condition) return true;
      
      switch (condition.type) {
        case 'parameter':
          const paramIndex = parseInt(condition.expression);
          return args[paramIndex] === condition.value;
        
        case 'call_count':
          const currentCount = this.getCallCount(condition.expression as keyof T);
          return eval(`${currentCount} ${condition.expression}`);
        
        case 'custom':
          return eval(condition.expression);
        
        default:
          return true;
      }
    }
    
    private executeBehavior(action: MockAction, args: any[]): any {
      switch (action.type) {
        case 'return':
          return action.value;
        
        case 'throw':
          throw action.value;
        
        case 'callback':
          if (action.callback) {
            return action.callback(...args);
          }
          break;
        
        case 'delay':
          return new Promise(resolve => {
            setTimeout(() => resolve(action.value), action.delay || 0);
          });
        
        case 'custom':
          if (action.callback) {
            return action.callback(...args);
          }
          break;
      }
      
      return undefined;
    }
  }
  
  // 📊 Method behavior builder
  export class MethodBehaviorBuilder<T> {
    constructor(
      private methodName: string,
      private mockFn: jest.Mock,
      private behaviors: Map<string, MockBehavior[]>
    ) {}
    
    withArgs(...args: any[]): ReturnBehaviorBuilder<T> {
      return new ReturnBehaviorBuilder(
        this.methodName,
        this.mockFn,
        this.behaviors,
        {
          type: 'parameter',
          expression: '0', // First parameter
          value: args[0]
        }
      );
    }
    
    onCallCount(count: number): ReturnBehaviorBuilder<T> {
      return new ReturnBehaviorBuilder(
        this.methodName,
        this.mockFn,
        this.behaviors,
        {
          type: 'call_count',
          expression: `== ${count}`,
          value: count
        }
      );
    }
    
    when(condition: string): ReturnBehaviorBuilder<T> {
      return new ReturnBehaviorBuilder(
        this.methodName,
        this.mockFn,
        this.behaviors,
        {
          type: 'custom',
          expression: condition
        }
      );
    }
    
    always(): ReturnBehaviorBuilder<T> {
      return new ReturnBehaviorBuilder(
        this.methodName,
        this.mockFn,
        this.behaviors
      );
    }
  }
  
  // 🎯 Return behavior builder
  export class ReturnBehaviorBuilder<T> {
    constructor(
      private methodName: string,
      private mockFn: jest.Mock,
      private behaviors: Map<string, MockBehavior[]>,
      private condition?: MockCondition
    ) {}
    
    returns(value: T): AdvancedMockBuilder<any> {
      this.addBehavior({
        type: 'return',
        value
      });
      return this as any;
    }
    
    throws(error: Error): AdvancedMockBuilder<any> {
      this.addBehavior({
        type: 'throw',
        value: error
      });
      return this as any;
    }
    
    calls(callback: (...args: any[]) => T): AdvancedMockBuilder<any> {
      this.addBehavior({
        type: 'callback',
        callback
      });
      return this as any;
    }
    
    resolves(value: T): AdvancedMockBuilder<any> {
      this.addBehavior({
        type: 'return',
        value: Promise.resolve(value)
      });
      return this as any;
    }
    
    rejects(error: Error): AdvancedMockBuilder<any> {
      this.addBehavior({
        type: 'return',
        value: Promise.reject(error)
      });
      return this as any;
    }
    
    after(delay: number): DelayedBehaviorBuilder<T> {
      return new DelayedBehaviorBuilder(
        this.methodName,
        this.mockFn,
        this.behaviors,
        this.condition,
        delay
      );
    }
    
    private addBehavior(action: MockAction): void {
      const behavior: MockBehavior = {
        condition: this.condition,
        action,
        priority: 1,
        description: `${this.methodName} behavior`
      };
      
      const existingBehaviors = this.behaviors.get(this.methodName) || [];
      existingBehaviors.push(behavior);
      this.behaviors.set(this.methodName, existingBehaviors);
    }
  }
  
  // ⏰ Delayed behavior builder
  export class DelayedBehaviorBuilder<T> {
    constructor(
      private methodName: string,
      private mockFn: jest.Mock,
      private behaviors: Map<string, MockBehavior[]>,
      private condition: MockCondition | undefined,
      private delay: number
    ) {}
    
    returns(value: T): AdvancedMockBuilder<any> {
      this.addBehavior({
        type: 'delay',
        value,
        delay: this.delay
      });
      return this as any;
    }
    
    throws(error: Error): AdvancedMockBuilder<any> {
      this.addBehavior({
        type: 'delay',
        value: Promise.reject(error),
        delay: this.delay
      });
      return this as any;
    }
    
    private addBehavior(action: MockAction): void {
      const behavior: MockBehavior = {
        condition: this.condition,
        action,
        priority: 1,
        description: `${this.methodName} delayed behavior`
      };
      
      const existingBehaviors = this.behaviors.get(this.methodName) || [];
      existingBehaviors.push(behavior);
      this.behaviors.set(this.methodName, existingBehaviors);
    }
  }
  
  // 📋 Supporting interfaces
  interface CallRecord {
    timestamp: Date;
    arguments: any[];
    stackTrace: string;
  }
  
  // 🏭 Mock factory with templates
  export class MockFactory {
    private static templates: Map<string, MockTemplate> = new Map();
    
    static registerTemplate<T>(name: string, template: MockTemplate): void {
      MockFactory.templates.set(name, template);
    }
    
    static createFromTemplate<T>(templateName: string): AdvancedMockBuilder<T> {
      const template = MockFactory.templates.get(templateName);
      if (!template) {
        throw new Error(`Template ${templateName} not found`);
      }
      
      const mockTemplate: Partial<T> = {};
      
      // Add methods from template
      template.defaultMethods.forEach(method => {
        (mockTemplate as any)[method.name] = jest.fn();
      });
      
      // Add properties from template
      template.defaultProperties.forEach(property => {
        (mockTemplate as any)[property.name] = property.value;
      });
      
      return new AdvancedMockBuilder<T>(mockTemplate);
    }
    
    static create<T>(template: Partial<T>): AdvancedMockBuilder<T> {
      return new AdvancedMockBuilder<T>(template);
    }
  }
  
  // 🎬 Scenario manager
  export class ScenarioManager {
    private scenarios: Map<string, MockScenario> = new Map();
    
    defineScenario(name: string, scenario: MockScenario): void {
      this.scenarios.set(name, scenario);
    }
    
    getScenario(name: string): MockScenario | undefined {
      return this.scenarios.get(name);
    }
    
    listScenarios(): string[] {
      return Array.from(this.scenarios.keys());
    }
    
    createStandardScenarios(): void {
      // Success scenario
      this.defineScenario('success', {
        name: 'success',
        description: 'All operations succeed',
        mocks: new Map([
          ['connect', [{
            action: { type: 'return', value: Promise.resolve() },
            priority: 1,
            description: 'Successful connection'
          }]],
          ['query', [{
            action: { type: 'return', value: Promise.resolve([]) },
            priority: 1,
            description: 'Successful query'
          }]]
        ]),
        setup: [],
        teardown: [],
        tags: ['success', 'happy-path']
      });
      
      // Failure scenario
      this.defineScenario('failure', {
        name: 'failure',
        description: 'Operations fail with errors',
        mocks: new Map([
          ['connect', [{
            action: { type: 'throw', value: new Error('Connection failed') },
            priority: 1,
            description: 'Connection failure'
          }]],
          ['query', [{
            action: { type: 'throw', value: new Error('Query failed') },
            priority: 1,
            description: 'Query failure'
          }]]
        ]),
        setup: [],
        teardown: [],
        tags: ['failure', 'error-handling']
      });
      
      // Timeout scenario
      this.defineScenario('timeout', {
        name: 'timeout',
        description: 'Operations timeout',
        mocks: new Map([
          ['connect', [{
            action: { type: 'delay', delay: 10000 },
            priority: 1,
            description: 'Connection timeout'
          }]]
        ]),
        setup: [],
        teardown: [],
        tags: ['timeout', 'performance']
      });
    }
  }
}

// 🚀 Usage examples with the advanced mock system
describe('Advanced Mock System Examples', () => {
  let mockFactory: MockManagement.MockFactory;
  let scenarioManager: MockManagement.ScenarioManager;
  
  beforeEach(() => {
    mockFactory = new MockManagement.MockFactory();
    scenarioManager = new MockManagement.ScenarioManager();
    scenarioManager.createStandardScenarios();
  });
  
  it('should create sophisticated mocks with fluent interface', async () => {
    // Create mock with complex behavior
    const mockDb = MockManagement.MockFactory
      .create<DatabaseConnection>({
        connect: jest.fn(),
        disconnect: jest.fn(),
        query: jest.fn(),
        transaction: jest.fn(),
        isConnected: jest.fn()
      })
      .whenCalled('connect')
        .always()
        .resolves(undefined)
      .whenCalled('query')
        .withArgs('SELECT * FROM users')
        .resolves([{ id: '1', name: 'John' }])
      .whenCalled('query')
        .withArgs('SELECT * FROM products')
        .after(1000)
        .resolves([{ id: '1', name: 'Product' }])
      .whenCalled('isConnected')
        .always()
        .returns(true)
      .build();
    
    // Test the mock
    await mockDb.connect();
    expect(mockDb.isConnected()).toBe(true);
    
    const users = await mockDb.query('SELECT * FROM users');
    expect(users).toEqual([{ id: '1', name: 'John' }]);
    
    // Test delayed response
    const startTime = Date.now();
    const products = await mockDb.query('SELECT * FROM products');
    const endTime = Date.now();
    
    expect(products).toEqual([{ id: '1', name: 'Product' }]);
    expect(endTime - startTime).toBeGreaterThanOrEqual(1000);
  });
  
  it('should apply scenarios to mocks', async () => {
    const successScenario = scenarioManager.getScenario('success');
    const failureScenario = scenarioManager.getScenario('failure');
    
    // Test success scenario
    const successMock = MockManagement.MockFactory
      .create<DatabaseConnection>({
        connect: jest.fn(),
        query: jest.fn(),
        disconnect: jest.fn(),
        transaction: jest.fn(),
        isConnected: jest.fn()
      })
      .applyScenario(successScenario!)
      .build();
    
    await expect(successMock.connect()).resolves.toBeUndefined();
    await expect(successMock.query('SELECT 1')).resolves.toEqual([]);
    
    // Test failure scenario
    const failureMock = MockManagement.MockFactory
      .create<DatabaseConnection>({
        connect: jest.fn(),
        query: jest.fn(),
        disconnect: jest.fn(),
        transaction: jest.fn(),
        isConnected: jest.fn()
      })
      .applyScenario(failureScenario!)
      .build();
    
    await expect(failureMock.connect()).rejects.toThrow('Connection failed');
    await expect(failureMock.query('SELECT 1')).rejects.toThrow('Query failed');
  });
  
  it('should track call history and provide verification methods', async () => {
    const mockBuilder = MockManagement.MockFactory
      .create<DatabaseConnection>({
        connect: jest.fn(),
        query: jest.fn(),
        disconnect: jest.fn(),
        transaction: jest.fn(),
        isConnected: jest.fn()
      })
      .whenCalled('query')
        .always()
        .resolves([]);
    
    const mock = mockBuilder.build();
    
    // Make some calls
    await mock.query('SELECT * FROM users');
    await mock.query('SELECT * FROM products', ['param1']);
    
    // Verify call history
    expect(mockBuilder.getCallCount('query')).toBe(2);
    expect(mockBuilder.wasCalledWith('query', 'SELECT * FROM users')).toBe(true);
    expect(mockBuilder.wasCalledWith('query', 'SELECT * FROM products', ['param1'])).toBe(true);
    expect(mockBuilder.wasCalledWith('query', 'SELECT * FROM orders')).toBe(false);
    
    const callHistory = mockBuilder.getCallHistory('query');
    expect(callHistory).toHaveLength(2);
    expect(callHistory[0].arguments).toEqual(['SELECT * FROM users']);
    expect(callHistory[1].arguments).toEqual(['SELECT * FROM products', ['param1']]);
  });
});

🎯 Conclusion

Congratulations! You’ve now mastered the sophisticated art of type-safe mocking in TypeScript! 🎉

Throughout this tutorial, you’ve learned how to:

  • Create comprehensive type-safe mocks that leverage TypeScript’s type system for better reliability and developer experience
  • Build advanced mocking patterns including fluent interfaces, behavior builders, and scenario management
  • Handle complex scenarios like async operations, error conditions, and sophisticated class hierarchies
  • Implement mock management systems that scale with large applications and complex test suites

Type-safe mocking in TypeScript is incredibly powerful because it combines the isolation benefits of traditional mocking with the safety and productivity of static typing. Your mocks provide IntelliSense, catch interface changes during refactoring, and help you write more maintainable tests.

Remember: good mocks should be simple, focused, and closely mirror the interfaces they’re replacing. They should make your tests faster, more isolated, and easier to understand. With TypeScript’s help, you can create mocks that are not only functionally correct but also provide excellent development experience and catch errors early.

The advanced mock management system you’ve learned enables you to handle complex scenarios systematically, reuse mock configurations across tests, and maintain consistency in large codebases. This approach scales beautifully as your application grows and testing requirements become more sophisticated.

Keep practicing these patterns, and you’ll find that type-safe mocking becomes an invaluable tool for creating robust, maintainable test suites that give you confidence in your TypeScript applications! 🚀

📚 Additional Resources