+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 146 of 355

πŸ— ️ Testing Classes: OOP Testing Patterns

Master object-oriented testing patterns in TypeScript, including class testing, inheritance, dependency injection, and advanced OOP scenarios πŸš€

πŸš€Intermediate
26 min read

Prerequisites

  • Understanding of TypeScript classes and object-oriented programming πŸ“
  • Knowledge of unit testing fundamentals and Jest ⚑
  • Familiarity with dependency injection and design patterns πŸ’»

What you'll learn

  • Test TypeScript classes with comprehensive coverage of methods and properties 🎯
  • Master testing patterns for inheritance, composition, and polymorphism πŸ—οΈ
  • Handle dependency injection and mocking in object-oriented architectures πŸ›
  • Create maintainable test suites for complex class hierarchies ✨

🎯 Introduction

Welcome to the architectural workshop of TypeScript class testing! πŸ—οΈ If testing regular functions were like inspecting individual building materials, then testing classes would be like evaluating entire architectural structures - complete with foundations (constructors), load-bearing walls (methods), electrical systems (properties), and complex interconnections (inheritance and composition) that all need to work together harmoniously to create a stable, functional building!

Object-oriented programming introduces unique testing challenges: state management, method interactions, inheritance hierarchies, encapsulation boundaries, and complex dependencies between objects. In TypeScript, we have the additional power of interfaces, access modifiers, and strong typing to help us create robust class-based architectures.

By the end of this tutorial, you’ll be a master architect of class testing, capable of thoroughly testing everything from simple data classes to complex enterprise-level object hierarchies. You’ll learn to test state mutations, method interactions, inheritance patterns, and dependency relationships while maintaining clean, maintainable test code. Let’s build some bulletproof class tests! 🌟

πŸ“š Understanding Class Testing Fundamentals

πŸ€” Why Class Testing Is Special

Classes encapsulate state and behavior, creating unique testing challenges around state mutations, method interactions, and object lifecycle management.

// 🌟 Setting up comprehensive class testing environment

// Basic class example with various testing scenarios
class BankAccount {
  private _balance: number;
  private _transactions: Transaction[];
  private _accountNumber: string;
  private _isLocked: boolean = false;
  private _dailyWithdrawalLimit: number = 1000;
  private _dailyWithdrawals: number = 0;
  private _lastTransactionDate: Date;

  constructor(
    initialBalance: number = 0,
    accountNumber?: string
  ) {
    if (initialBalance < 0) {
      throw new Error('Initial balance cannot be negative');
    }
    
    this._balance = initialBalance;
    this._accountNumber = accountNumber || this.generateAccountNumber();
    this._transactions = [];
    this._lastTransactionDate = new Date();
  }

  // Getter methods
  get balance(): number {
    return this._balance;
  }

  get accountNumber(): string {
    return this._accountNumber;
  }

  get isLocked(): boolean {
    return this._isLocked;
  }

  get transactionHistory(): readonly Transaction[] {
    return [...this._transactions];
  }

  get dailyWithdrawalsRemaining(): number {
    return Math.max(0, this._dailyWithdrawalLimit - this._dailyWithdrawals);
  }

  // State-changing methods
  deposit(amount: number, description: string = 'Deposit'): void {
    this.validateAmount(amount);
    this.checkAccountLocked();
    
    const oldBalance = this._balance;
    this._balance += amount;
    
    this.recordTransaction({
      type: 'deposit',
      amount,
      description,
      balanceBefore: oldBalance,
      balanceAfter: this._balance,
      timestamp: new Date()
    });
  }

  withdraw(amount: number, description: string = 'Withdrawal'): void {
    this.validateAmount(amount);
    this.checkAccountLocked();
    this.checkSufficientFunds(amount);
    this.checkDailyLimit(amount);
    
    const oldBalance = this._balance;
    this._balance -= amount;
    this._dailyWithdrawals += amount;
    
    this.recordTransaction({
      type: 'withdrawal',
      amount,
      description,
      balanceBefore: oldBalance,
      balanceAfter: this._balance,
      timestamp: new Date()
    });
  }

  transfer(amount: number, targetAccount: BankAccount, description: string = 'Transfer'): void {
    this.withdraw(amount, `${description} to ${targetAccount.accountNumber}`);
    targetAccount.deposit(amount, `${description} from ${this.accountNumber}`);
  }

  // Account management methods
  lockAccount(): void {
    this._isLocked = true;
  }

  unlockAccount(): void {
    this._isLocked = false;
  }

  setDailyWithdrawalLimit(limit: number): void {
    if (limit < 0) {
      throw new Error('Daily withdrawal limit cannot be negative');
    }
    this._dailyWithdrawalLimit = limit;
  }

  resetDailyWithdrawals(): void {
    this._dailyWithdrawals = 0;
  }

  // Query methods
  getTransactionsByType(type: TransactionType): Transaction[] {
    return this._transactions.filter(t => t.type === type);
  }

  getTransactionsByDateRange(start: Date, end: Date): Transaction[] {
    return this._transactions.filter(t => 
      t.timestamp >= start && t.timestamp <= end
    );
  }

  calculateInterest(rate: number, days: number): number {
    return (this._balance * rate * days) / 365;
  }

  // Private helper methods
  private validateAmount(amount: number): void {
    if (amount <= 0) {
      throw new Error('Amount must be positive');
    }
    if (!Number.isFinite(amount)) {
      throw new Error('Amount must be a valid number');
    }
  }

  private checkAccountLocked(): void {
    if (this._isLocked) {
      throw new Error('Account is locked');
    }
  }

  private checkSufficientFunds(amount: number): void {
    if (amount > this._balance) {
      throw new Error('Insufficient funds');
    }
  }

  private checkDailyLimit(amount: number): void {
    if (this._dailyWithdrawals + amount > this._dailyWithdrawalLimit) {
      throw new Error('Daily withdrawal limit exceeded');
    }
  }

  private recordTransaction(transaction: Omit<Transaction, 'id'>): void {
    const fullTransaction: Transaction = {
      id: this.generateTransactionId(),
      ...transaction
    };
    
    this._transactions.push(fullTransaction);
    this._lastTransactionDate = transaction.timestamp;
  }

  private generateAccountNumber(): string {
    return Math.random().toString(36).substr(2, 10).toUpperCase();
  }

  private generateTransactionId(): string {
    return `TXN-${Date.now()}-${Math.random().toString(36).substr(2, 6)}`;
  }
}

// Supporting types and interfaces
type TransactionType = 'deposit' | 'withdrawal';

interface Transaction {
  id: string;
  type: TransactionType;
  amount: number;
  description: string;
  balanceBefore: number;
  balanceAfter: number;
  timestamp: Date;
}

// More complex class with dependencies
interface Logger {
  log(message: string, level?: 'info' | 'warn' | 'error'): void;
  getLastLogMessage(): string | null;
}

interface NotificationService {
  sendNotification(accountNumber: string, message: string): Promise<void>;
  getNotificationHistory(accountNumber: string): string[];
}

interface AuditService {
  recordEvent(event: AuditEvent): void;
  getAuditTrail(accountNumber: string): AuditEvent[];
}

interface AuditEvent {
  accountNumber: string;
  action: string;
  timestamp: Date;
  details: any;
}

class PremiumBankAccount extends BankAccount {
  private _overdraftLimit: number;
  private _interestRate: number;
  private _premiumFeatures: Set<string>;

  constructor(
    initialBalance: number,
    accountNumber: string,
    private logger: Logger,
    private notificationService: NotificationService,
    private auditService: AuditService,
    overdraftLimit: number = 500,
    interestRate: number = 0.02
  ) {
    super(initialBalance, accountNumber);
    this._overdraftLimit = overdraftLimit;
    this._interestRate = interestRate;
    this._premiumFeatures = new Set(['overdraft', 'priority_support', 'monthly_statements']);
    
    this.logger.log(`Premium account created: ${accountNumber}`, 'info');
    this.auditService.recordEvent({
      accountNumber,
      action: 'account_created',
      timestamp: new Date(),
      details: { initialBalance, overdraftLimit, interestRate }
    });
  }

  // Override withdrawal with overdraft support
  withdraw(amount: number, description: string = 'Withdrawal'): void {
    this.validateAmount(amount);
    this.checkAccountLocked();
    this.checkOverdraftLimit(amount);
    
    const oldBalance = this.balance;
    const availableFunds = this.balance + this._overdraftLimit;
    
    if (amount > availableFunds) {
      throw new Error('Exceeds available funds including overdraft');
    }

    // Use parent's private method indirectly by accessing balance
    const newBalance = this.balance - amount;
    
    // Record transaction and audit
    this.auditService.recordEvent({
      accountNumber: this.accountNumber,
      action: 'withdrawal',
      timestamp: new Date(),
      details: { amount, description, oldBalance, newBalance }
    });

    // Call parent's deposit/withdraw to update balance
    // This is a workaround since we can't access private methods directly
    if (newBalance >= 0) {
      super.withdraw(amount, description);
    } else {
      // Handle overdraft scenario
      this.handleOverdraftWithdrawal(amount, description, oldBalance);
    }

    // Send notification for large withdrawals
    if (amount > 1000) {
      this.notificationService.sendNotification(
        this.accountNumber,
        `Large withdrawal of ${amount} processed`
      ).catch(error => {
        this.logger.log(`Failed to send notification: ${error.message}`, 'error');
      });
    }
  }

  // Premium-specific methods
  applyMonthlyInterest(): number {
    if (this.balance <= 0) return 0;
    
    const interest = this.calculateInterest(this._interestRate, 30);
    this.deposit(interest, 'Monthly interest');
    
    this.logger.log(`Interest applied: ${interest}`, 'info');
    this.auditService.recordEvent({
      accountNumber: this.accountNumber,
      action: 'interest_applied',
      timestamp: new Date(),
      details: { amount: interest, rate: this._interestRate }
    });

    return interest;
  }

  upgradeFeature(feature: string): void {
    if (this._premiumFeatures.has(feature)) {
      throw new Error(`Feature ${feature} already enabled`);
    }
    
    this._premiumFeatures.add(feature);
    this.logger.log(`Feature enabled: ${feature}`, 'info');
    
    this.auditService.recordEvent({
      accountNumber: this.accountNumber,
      action: 'feature_upgraded',
      timestamp: new Date(),
      details: { feature }
    });
  }

  hasFeature(feature: string): boolean {
    return this._premiumFeatures.has(feature);
  }

  getAvailableBalance(): number {
    return this.balance + this._overdraftLimit;
  }

  getOverdraftUsed(): number {
    return Math.max(0, -this.balance);
  }

  // Getters
  get overdraftLimit(): number {
    return this._overdraftLimit;
  }

  get interestRate(): number {
    return this._interestRate;
  }

  get premiumFeatures(): readonly string[] {
    return Array.from(this._premiumFeatures);
  }

  // Private methods
  private checkOverdraftLimit(amount: number): void {
    const wouldExceedOverdraft = (amount - this.balance) > this._overdraftLimit;
    if (wouldExceedOverdraft) {
      throw new Error('Would exceed overdraft limit');
    }
  }

  private handleOverdraftWithdrawal(amount: number, description: string, oldBalance: number): void {
    // Complex overdraft handling logic
    const overdraftUsed = amount - oldBalance;
    
    this.logger.log(
      `Overdraft used: ${overdraftUsed} for withdrawal of ${amount}`,
      'warn'
    );

    // This would need to access parent's protected/private methods
    // In a real implementation, we'd need proper access modifiers
  }
}

// Abstract base class for testing inheritance
abstract class Account {
  protected _accountId: string;
  protected _owner: string;
  protected _createdAt: Date;

  constructor(accountId: string, owner: string) {
    this._accountId = accountId;
    this._owner = owner;
    this._createdAt = new Date();
  }

  get accountId(): string {
    return this._accountId;
  }

  get owner(): string {
    return this._owner;
  }

  get createdAt(): Date {
    return this._createdAt;
  }

  abstract getBalance(): number;
  abstract deposit(amount: number): void;
  abstract withdraw(amount: number): void;

  protected validateOwner(owner: string): void {
    if (!owner || owner.trim().length === 0) {
      throw new Error('Owner name is required');
    }
  }

  getAccountInfo(): string {
    return `Account ${this._accountId} owned by ${this._owner}`;
  }
}

// Concrete implementation for testing
class SavingsAccount extends Account {
  private _balance: number = 0;
  private _interestRate: number;

  constructor(accountId: string, owner: string, interestRate: number = 0.01) {
    super(accountId, owner);
    this.validateOwner(owner);
    this._interestRate = interestRate;
  }

  getBalance(): number {
    return this._balance;
  }

  deposit(amount: number): void {
    if (amount <= 0) {
      throw new Error('Deposit amount must be positive');
    }
    this._balance += amount;
  }

  withdraw(amount: number): void {
    if (amount <= 0) {
      throw new Error('Withdrawal amount must be positive');
    }
    if (amount > this._balance) {
      throw new Error('Insufficient funds');
    }
    this._balance -= amount;
  }

  calculateInterest(): number {
    return this._balance * this._interestRate;
  }

  applyInterest(): void {
    const interest = this.calculateInterest();
    this.deposit(interest);
  }
}

πŸ§ͺ Testing Basic Class Functionality

βœ… Constructor and Property Testing

Testing how classes are initialized and how their properties behave is foundational to class testing.

// 🌟 Comprehensive class constructor and property testing

describe('BankAccount - Constructor and Properties', () => {
  // βœ… Testing valid constructor scenarios
  describe('Constructor', () => {
    it('should create account with default initial balance', () => {
      // Act
      const account = new BankAccount();

      // Assert
      expect(account.balance).toBe(0);
      expect(account.accountNumber).toBeDefined();
      expect(account.accountNumber).toMatch(/^[A-Z0-9]{10}$/);
      expect(account.isLocked).toBe(false);
      expect(account.transactionHistory).toEqual([]);
    });

    it('should create account with specified initial balance', () => {
      // Arrange
      const initialBalance = 1000;

      // Act
      const account = new BankAccount(initialBalance);

      // Assert
      expect(account.balance).toBe(initialBalance);
      expect(account.transactionHistory).toEqual([]);
    });

    it('should create account with custom account number', () => {
      // Arrange
      const accountNumber = 'CUSTOM123';
      const initialBalance = 500;

      // Act
      const account = new BankAccount(initialBalance, accountNumber);

      // Assert
      expect(account.accountNumber).toBe(accountNumber);
      expect(account.balance).toBe(initialBalance);
    });

    it('should throw error for negative initial balance', () => {
      // Act & Assert
      expect(() => new BankAccount(-100))
        .toThrow('Initial balance cannot be negative');
    });

    it('should generate unique account numbers', () => {
      // Act
      const account1 = new BankAccount();
      const account2 = new BankAccount();
      const account3 = new BankAccount();

      // Assert
      expect(account1.accountNumber).not.toBe(account2.accountNumber);
      expect(account2.accountNumber).not.toBe(account3.accountNumber);
      expect(account1.accountNumber).not.toBe(account3.accountNumber);
    });
  });

  // βœ… Testing property getters
  describe('Property Getters', () => {
    let account: BankAccount;

    beforeEach(() => {
      account = new BankAccount(1000, 'TEST123');
    });

    it('should return correct balance', () => {
      expect(account.balance).toBe(1000);
    });

    it('should return correct account number', () => {
      expect(account.accountNumber).toBe('TEST123');
    });

    it('should return correct locked status', () => {
      expect(account.isLocked).toBe(false);
      
      account.lockAccount();
      expect(account.isLocked).toBe(true);
    });

    it('should return empty transaction history initially', () => {
      expect(account.transactionHistory).toEqual([]);
      expect(account.transactionHistory).toHaveLength(0);
    });

    it('should return correct daily withdrawals remaining', () => {
      expect(account.dailyWithdrawalsRemaining).toBe(1000);
      
      account.withdraw(200);
      expect(account.dailyWithdrawalsRemaining).toBe(800);
    });

    it('should return immutable transaction history', () => {
      // Act
      const history = account.transactionHistory;
      
      // Try to modify the returned array
      expect(() => {
        (history as any).push({ fake: 'transaction' });
      }).toThrow();
    });
  });

  // βœ… Testing property state changes
  describe('Property State Changes', () => {
    let account: BankAccount;

    beforeEach(() => {
      account = new BankAccount(1000);
    });

    it('should update balance after deposit', () => {
      // Arrange
      const initialBalance = account.balance;
      const depositAmount = 500;

      // Act
      account.deposit(depositAmount);

      // Assert
      expect(account.balance).toBe(initialBalance + depositAmount);
    });

    it('should update balance after withdrawal', () => {
      // Arrange
      const initialBalance = account.balance;
      const withdrawalAmount = 300;

      // Act
      account.withdraw(withdrawalAmount);

      // Assert
      expect(account.balance).toBe(initialBalance - withdrawalAmount);
    });

    it('should update locked status', () => {
      // Initially unlocked
      expect(account.isLocked).toBe(false);

      // Lock account
      account.lockAccount();
      expect(account.isLocked).toBe(true);

      // Unlock account
      account.unlockAccount();
      expect(account.isLocked).toBe(false);
    });

    it('should update transaction history after operations', () => {
      // Initially empty
      expect(account.transactionHistory).toHaveLength(0);

      // After deposit
      account.deposit(500, 'Test deposit');
      expect(account.transactionHistory).toHaveLength(1);
      expect(account.transactionHistory[0].type).toBe('deposit');
      expect(account.transactionHistory[0].amount).toBe(500);

      // After withdrawal
      account.withdraw(200, 'Test withdrawal');
      expect(account.transactionHistory).toHaveLength(2);
      expect(account.transactionHistory[1].type).toBe('withdrawal');
      expect(account.transactionHistory[1].amount).toBe(200);
    });
  });
});

// Testing complex property interactions
describe('BankAccount - Property Interactions', () => {
  let account: BankAccount;

  beforeEach(() => {
    account = new BankAccount(1000);
  });

  it('should maintain consistent state across multiple operations', () => {
    // Track state throughout operations
    const operations = [
      { type: 'deposit', amount: 500 },
      { type: 'withdraw', amount: 200 },
      { type: 'deposit', amount: 300 },
      { type: 'withdraw', amount: 100 }
    ];

    let expectedBalance = 1000;

    operations.forEach(op => {
      if (op.type === 'deposit') {
        account.deposit(op.amount);
        expectedBalance += op.amount;
      } else {
        account.withdraw(op.amount);
        expectedBalance -= op.amount;
      }

      // Verify state consistency after each operation
      expect(account.balance).toBe(expectedBalance);
      expect(account.transactionHistory).toHaveLength(
        operations.indexOf(op) + 1
      );
    });

    // Final state verification
    expect(account.balance).toBe(1500);
    expect(account.transactionHistory).toHaveLength(4);
  });

  it('should handle daily withdrawal limit correctly', () => {
    // Set a custom daily limit
    account.setDailyWithdrawalLimit(600);
    expect(account.dailyWithdrawalsRemaining).toBe(600);

    // First withdrawal
    account.withdraw(300);
    expect(account.dailyWithdrawalsRemaining).toBe(300);

    // Second withdrawal
    account.withdraw(200);
    expect(account.dailyWithdrawalsRemaining).toBe(100);

    // Should reject withdrawal that exceeds daily limit
    expect(() => account.withdraw(200))
      .toThrow('Daily withdrawal limit exceeded');

    // Reset daily withdrawals
    account.resetDailyWithdrawals();
    expect(account.dailyWithdrawalsRemaining).toBe(600);
  });

  it('should maintain transaction history integrity', () => {
    // Perform various operations
    account.deposit(500, 'Salary');
    account.withdraw(200, 'Groceries');
    account.deposit(100, 'Refund');

    const history = account.transactionHistory;

    // Verify transaction details
    expect(history[0]).toMatchObject({
      type: 'deposit',
      amount: 500,
      description: 'Salary',
      balanceBefore: 1000,
      balanceAfter: 1500
    });

    expect(history[1]).toMatchObject({
      type: 'withdrawal',
      amount: 200,
      description: 'Groceries',
      balanceBefore: 1500,
      balanceAfter: 1300
    });

    expect(history[2]).toMatchObject({
      type: 'deposit',
      amount: 100,
      description: 'Refund',
      balanceBefore: 1300,
      balanceAfter: 1400
    });

    // Verify all transactions have required fields
    history.forEach(transaction => {
      expect(transaction.id).toBeDefined();
      expect(transaction.timestamp).toBeInstanceOf(Date);
      expect(transaction.type).toMatch(/^(deposit|withdrawal)$/);
      expect(transaction.amount).toBeGreaterThan(0);
    });
  });
});

πŸ”€ Testing Method Interactions and State Changes

🎯 Comprehensive Method Testing

Methods are the behavior of classes, and testing them requires verifying both their direct effects and side effects.

// 🌟 Comprehensive method testing patterns

describe('BankAccount - Method Testing', () => {
  let account: BankAccount;

  beforeEach(() => {
    account = new BankAccount(1000, 'TEST123');
  });

  // βœ… Testing deposit method
  describe('deposit method', () => {
    it('should increase balance by deposit amount', () => {
      // Arrange
      const initialBalance = account.balance;
      const depositAmount = 500;

      // Act
      account.deposit(depositAmount);

      // Assert
      expect(account.balance).toBe(initialBalance + depositAmount);
    });

    it('should add transaction to history', () => {
      // Arrange
      const depositAmount = 250;
      const description = 'Test deposit';

      // Act
      account.deposit(depositAmount, description);

      // Assert
      const transactions = account.transactionHistory;
      expect(transactions).toHaveLength(1);
      expect(transactions[0]).toMatchObject({
        type: 'deposit',
        amount: depositAmount,
        description,
        balanceBefore: 1000,
        balanceAfter: 1250
      });
      expect(transactions[0].id).toBeDefined();
      expect(transactions[0].timestamp).toBeInstanceOf(Date);
    });

    it('should use default description when not provided', () => {
      // Act
      account.deposit(100);

      // Assert
      const transaction = account.transactionHistory[0];
      expect(transaction.description).toBe('Deposit');
    });

    it('should reject zero amount', () => {
      expect(() => account.deposit(0))
        .toThrow('Amount must be positive');
    });

    it('should reject negative amount', () => {
      expect(() => account.deposit(-100))
        .toThrow('Amount must be positive');
    });

    it('should reject invalid amounts', () => {
      expect(() => account.deposit(NaN))
        .toThrow('Amount must be a valid number');
      
      expect(() => account.deposit(Infinity))
        .toThrow('Amount must be a valid number');
    });

    it('should reject deposits when account is locked', () => {
      // Arrange
      account.lockAccount();

      // Act & Assert
      expect(() => account.deposit(100))
        .toThrow('Account is locked');
      
      // Verify balance unchanged
      expect(account.balance).toBe(1000);
    });

    it('should handle multiple consecutive deposits', () => {
      // Arrange
      const deposits = [100, 200, 300, 150];
      let expectedBalance = 1000;

      // Act & Assert
      deposits.forEach((amount, index) => {
        account.deposit(amount);
        expectedBalance += amount;
        
        expect(account.balance).toBe(expectedBalance);
        expect(account.transactionHistory).toHaveLength(index + 1);
      });
    });
  });

  // βœ… Testing withdrawal method
  describe('withdraw method', () => {
    it('should decrease balance by withdrawal amount', () => {
      // Arrange
      const initialBalance = account.balance;
      const withdrawalAmount = 300;

      // Act
      account.withdraw(withdrawalAmount);

      // Assert
      expect(account.balance).toBe(initialBalance - withdrawalAmount);
    });

    it('should add transaction to history', () => {
      // Arrange
      const withdrawalAmount = 200;
      const description = 'ATM withdrawal';

      // Act
      account.withdraw(withdrawalAmount, description);

      // Assert
      const transactions = account.transactionHistory;
      expect(transactions).toHaveLength(1);
      expect(transactions[0]).toMatchObject({
        type: 'withdrawal',
        amount: withdrawalAmount,
        description,
        balanceBefore: 1000,
        balanceAfter: 800
      });
    });

    it('should track daily withdrawal amounts', () => {
      // Act
      account.withdraw(300);
      expect(account.dailyWithdrawalsRemaining).toBe(700);

      account.withdraw(200);
      expect(account.dailyWithdrawalsRemaining).toBe(500);
    });

    it('should reject withdrawal exceeding balance', () => {
      expect(() => account.withdraw(1500))
        .toThrow('Insufficient funds');
      
      // Verify no state change
      expect(account.balance).toBe(1000);
      expect(account.transactionHistory).toHaveLength(0);
    });

    it('should reject withdrawal exceeding daily limit', () => {
      // First withdrawal should succeed
      account.withdraw(800);
      expect(account.balance).toBe(200);

      // Second withdrawal should fail due to daily limit
      expect(() => account.withdraw(300))
        .toThrow('Daily withdrawal limit exceeded');
      
      // Balance should remain unchanged from failed withdrawal
      expect(account.balance).toBe(200);
    });

    it('should allow withdrawal up to exact daily limit', () => {
      // Withdraw exactly the daily limit
      account.withdraw(1000);
      
      expect(account.balance).toBe(0);
      expect(account.dailyWithdrawalsRemaining).toBe(0);
    });

    it('should reject withdrawals when account is locked', () => {
      // Arrange
      account.lockAccount();

      // Act & Assert
      expect(() => account.withdraw(100))
        .toThrow('Account is locked');
    });
  });

  // βœ… Testing transfer method
  describe('transfer method', () => {
    let targetAccount: BankAccount;

    beforeEach(() => {
      targetAccount = new BankAccount(500, 'TARGET456');
    });

    it('should transfer money between accounts', () => {
      // Arrange
      const transferAmount = 300;
      const sourceInitialBalance = account.balance;
      const targetInitialBalance = targetAccount.balance;

      // Act
      account.transfer(transferAmount, targetAccount, 'Test transfer');

      // Assert
      expect(account.balance).toBe(sourceInitialBalance - transferAmount);
      expect(targetAccount.balance).toBe(targetInitialBalance + transferAmount);
    });

    it('should create transactions in both accounts', () => {
      // Act
      account.transfer(200, targetAccount, 'Transfer test');

      // Assert source account
      const sourceTransactions = account.transactionHistory;
      expect(sourceTransactions).toHaveLength(1);
      expect(sourceTransactions[0]).toMatchObject({
        type: 'withdrawal',
        amount: 200,
        description: 'Transfer test to TARGET456'
      });

      // Assert target account
      const targetTransactions = targetAccount.transactionHistory;
      expect(targetTransactions).toHaveLength(1);
      expect(targetTransactions[0]).toMatchObject({
        type: 'deposit',
        amount: 200,
        description: 'Transfer test from TEST123'
      });
    });

    it('should fail if source account has insufficient funds', () => {
      // Act & Assert
      expect(() => account.transfer(1500, targetAccount))
        .toThrow('Insufficient funds');
      
      // Verify no state changes
      expect(account.balance).toBe(1000);
      expect(targetAccount.balance).toBe(500);
      expect(account.transactionHistory).toHaveLength(0);
      expect(targetAccount.transactionHistory).toHaveLength(0);
    });

    it('should fail if transfer would exceed daily limit', () => {
      // Arrange - make a withdrawal first to approach daily limit
      account.withdraw(800);

      // Act & Assert
      expect(() => account.transfer(300, targetAccount))
        .toThrow('Daily withdrawal limit exceeded');
    });

    it('should fail if source account is locked', () => {
      // Arrange
      account.lockAccount();

      // Act & Assert
      expect(() => account.transfer(100, targetAccount))
        .toThrow('Account is locked');
    });

    it('should fail if target account is locked', () => {
      // Arrange
      targetAccount.lockAccount();

      // Act & Assert
      expect(() => account.transfer(100, targetAccount))
        .toThrow('Account is locked');
      
      // Source should be debited but target deposit should fail
      // In a real implementation, this would need to be a transaction
    });
  });

  // βœ… Testing query methods
  describe('Query methods', () => {
    beforeEach(() => {
      // Set up some transaction history
      account.deposit(500, 'Salary');
      account.withdraw(200, 'Groceries');
      account.deposit(100, 'Refund');
      account.withdraw(150, 'Gas');
    });

    it('should filter transactions by type', () => {
      // Act
      const deposits = account.getTransactionsByType('deposit');
      const withdrawals = account.getTransactionsByType('withdrawal');

      // Assert
      expect(deposits).toHaveLength(2);
      expect(withdrawals).toHaveLength(2);
      
      deposits.forEach(tx => expect(tx.type).toBe('deposit'));
      withdrawals.forEach(tx => expect(tx.type).toBe('withdrawal'));
    });

    it('should filter transactions by date range', () => {
      // Arrange
      const now = new Date();
      const oneHourAgo = new Date(now.getTime() - 60 * 60 * 1000);
      const oneHourFromNow = new Date(now.getTime() + 60 * 60 * 1000);

      // Act
      const recentTransactions = account.getTransactionsByDateRange(
        oneHourAgo,
        oneHourFromNow
      );

      // Assert
      expect(recentTransactions).toHaveLength(4); // All transactions should be recent
      recentTransactions.forEach(tx => {
        expect(tx.timestamp.getTime()).toBeGreaterThanOrEqual(oneHourAgo.getTime());
        expect(tx.timestamp.getTime()).toBeLessThanOrEqual(oneHourFromNow.getTime());
      });
    });

    it('should calculate interest correctly', () => {
      // Arrange
      const rate = 0.05; // 5% annual rate
      const days = 365; // One year

      // Act
      const interest = account.calculateInterest(rate, days);

      // Assert
      const expectedInterest = account.balance * rate; // For one year
      expect(interest).toBeCloseTo(expectedInterest, 2);
    });

    it('should calculate interest for partial periods', () => {
      // Arrange
      const rate = 0.12; // 12% annual rate
      const days = 30; // One month

      // Act
      const interest = account.calculateInterest(rate, days);

      // Assert
      const expectedInterest = (account.balance * rate * days) / 365;
      expect(interest).toBeCloseTo(expectedInterest, 2);
    });
  });

  // βœ… Testing account management methods
  describe('Account management', () => {
    it('should lock and unlock account', () => {
      // Initially unlocked
      expect(account.isLocked).toBe(false);

      // Lock account
      account.lockAccount();
      expect(account.isLocked).toBe(true);

      // Unlock account
      account.unlockAccount();
      expect(account.isLocked).toBe(false);
    });

    it('should set daily withdrawal limit', () => {
      // Act
      account.setDailyWithdrawalLimit(2000);

      // Assert
      expect(account.dailyWithdrawalsRemaining).toBe(2000);
    });

    it('should reject negative daily withdrawal limit', () => {
      expect(() => account.setDailyWithdrawalLimit(-100))
        .toThrow('Daily withdrawal limit cannot be negative');
    });

    it('should reset daily withdrawals', () => {
      // Arrange - make some withdrawals
      account.withdraw(500);
      expect(account.dailyWithdrawalsRemaining).toBe(500);

      // Act
      account.resetDailyWithdrawals();

      // Assert
      expect(account.dailyWithdrawalsRemaining).toBe(1000);
    });
  });
});

πŸ—οΈ Testing Inheritance and Polymorphism

πŸ”— Testing Class Hierarchies

Inheritance introduces complexity in testing, requiring verification of both parent and child class behavior.

// 🌟 Comprehensive inheritance testing patterns

describe('Abstract Account - Base Class Testing', () => {
  // Since Account is abstract, we test through concrete implementation
  describe('SavingsAccount inheritance', () => {
    let account: SavingsAccount;

    beforeEach(() => {
      account = new SavingsAccount('SAV123', 'John Doe', 0.02);
    });

    // βœ… Testing inherited properties
    it('should inherit base class properties', () => {
      expect(account.accountId).toBe('SAV123');
      expect(account.owner).toBe('John Doe');
      expect(account.createdAt).toBeInstanceOf(Date);
    });

    // βœ… Testing abstract method implementation
    it('should implement abstract methods', () => {
      // Test getBalance implementation
      expect(account.getBalance()).toBe(0);

      // Test deposit implementation
      account.deposit(500);
      expect(account.getBalance()).toBe(500);

      // Test withdraw implementation
      account.withdraw(200);
      expect(account.getBalance()).toBe(300);
    });

    // βœ… Testing inherited methods
    it('should use inherited getAccountInfo method', () => {
      const info = account.getAccountInfo();
      expect(info).toBe('Account SAV123 owned by John Doe');
    });

    // βœ… Testing constructor validation from base class
    it('should validate owner through base class', () => {
      expect(() => new SavingsAccount('SAV456', '', 0.02))
        .toThrow('Owner name is required');
      
      expect(() => new SavingsAccount('SAV789', '   ', 0.02))
        .toThrow('Owner name is required');
    });

    // βœ… Testing subclass-specific functionality
    it('should calculate and apply interest', () => {
      // Arrange
      account.deposit(1000);
      const initialBalance = account.getBalance();

      // Act
      const expectedInterest = account.calculateInterest();
      account.applyInterest();

      // Assert
      expect(expectedInterest).toBe(1000 * 0.02);
      expect(account.getBalance()).toBe(initialBalance + expectedInterest);
    });
  });
});

describe('PremiumBankAccount - Complex Inheritance Testing', () => {
  let mockLogger: jest.Mocked<Logger>;
  let mockNotificationService: jest.Mocked<NotificationService>;
  let mockAuditService: jest.Mocked<AuditService>;
  let premiumAccount: PremiumBankAccount;

  beforeEach(() => {
    // Create mocks for dependencies
    mockLogger = {
      log: jest.fn(),
      getLastLogMessage: jest.fn()
    };

    mockNotificationService = {
      sendNotification: jest.fn(),
      getNotificationHistory: jest.fn()
    };

    mockAuditService = {
      recordEvent: jest.fn(),
      getAuditTrail: jest.fn()
    };

    premiumAccount = new PremiumBankAccount(
      1000,
      'PREM123',
      mockLogger,
      mockNotificationService,
      mockAuditService,
      500, // overdraft limit
      0.03 // interest rate
    );
  });

  // βœ… Testing constructor with dependency injection
  describe('Constructor with dependencies', () => {
    it('should initialize with dependencies and log creation', () => {
      expect(premiumAccount.balance).toBe(1000);
      expect(premiumAccount.accountNumber).toBe('PREM123');
      expect(premiumAccount.overdraftLimit).toBe(500);
      expect(premiumAccount.interestRate).toBe(0.03);

      // Verify logging
      expect(mockLogger.log).toHaveBeenCalledWith(
        'Premium account created: PREM123',
        'info'
      );

      // Verify audit
      expect(mockAuditService.recordEvent).toHaveBeenCalledWith({
        accountNumber: 'PREM123',
        action: 'account_created',
        timestamp: expect.any(Date),
        details: {
          initialBalance: 1000,
          overdraftLimit: 500,
          interestRate: 0.03
        }
      });
    });

    it('should initialize with default values', () => {
      const defaultAccount = new PremiumBankAccount(
        500,
        'DEFAULT',
        mockLogger,
        mockNotificationService,
        mockAuditService
      );

      expect(defaultAccount.overdraftLimit).toBe(500);
      expect(defaultAccount.interestRate).toBe(0.02);
    });
  });

  // βœ… Testing method overriding
  describe('Method overriding', () => {
    it('should override withdraw with overdraft support', () => {
      // Test normal withdrawal
      premiumAccount.withdraw(300);
      expect(premiumAccount.balance).toBe(700);

      // Test overdraft withdrawal
      premiumAccount.withdraw(900); // Should use overdraft
      expect(premiumAccount.balance).toBe(-200);
      expect(premiumAccount.getOverdraftUsed()).toBe(200);
    });

    it('should reject withdrawal exceeding overdraft limit', () => {
      expect(() => premiumAccount.withdraw(1600))
        .toThrow('Exceeds available funds including overdraft');
      
      expect(premiumAccount.balance).toBe(1000);
    });

    it('should send notifications for large withdrawals', async () => {
      // Arrange
      mockNotificationService.sendNotification.mockResolvedValue();

      // Act
      premiumAccount.withdraw(1200, 'Large purchase');

      // Assert - wait for async notification
      await new Promise(resolve => setTimeout(resolve, 0));
      
      expect(mockNotificationService.sendNotification).toHaveBeenCalledWith(
        'PREM123',
        'Large withdrawal of 1200 processed'
      );
    });

    it('should log errors when notification fails', async () => {
      // Arrange
      const notificationError = new Error('Service unavailable');
      mockNotificationService.sendNotification.mockRejectedValue(notificationError);

      // Act
      premiumAccount.withdraw(1200);

      // Assert - wait for async operation
      await new Promise(resolve => setTimeout(resolve, 10));
      
      expect(mockLogger.log).toHaveBeenCalledWith(
        'Failed to send notification: Service unavailable',
        'error'
      );
    });

    it('should record audit events for withdrawals', () => {
      // Act
      premiumAccount.withdraw(300, 'Test withdrawal');

      // Assert
      expect(mockAuditService.recordEvent).toHaveBeenCalledWith({
        accountNumber: 'PREM123',
        action: 'withdrawal',
        timestamp: expect.any(Date),
        details: {
          amount: 300,
          description: 'Test withdrawal',
          oldBalance: 1000,
          newBalance: 700
        }
      });
    });
  });

  // βœ… Testing premium-specific features
  describe('Premium features', () => {
    it('should apply monthly interest', () => {
      // Act
      const interest = premiumAccount.applyMonthlyInterest();

      // Assert
      const expectedInterest = premiumAccount.calculateInterest(0.03, 30);
      expect(interest).toBeCloseTo(expectedInterest, 2);
      expect(premiumAccount.balance).toBeCloseTo(1000 + expectedInterest, 2);

      // Verify logging and audit
      expect(mockLogger.log).toHaveBeenCalledWith(
        expect.stringContaining('Interest applied:'),
        'info'
      );
      expect(mockAuditService.recordEvent).toHaveBeenCalledWith({
        accountNumber: 'PREM123',
        action: 'interest_applied',
        timestamp: expect.any(Date),
        details: { amount: interest, rate: 0.03 }
      });
    });

    it('should not apply interest for negative balance', () => {
      // Arrange - create overdraft situation
      premiumAccount.withdraw(1200);
      expect(premiumAccount.balance).toBe(-200);

      // Act
      const interest = premiumAccount.applyMonthlyInterest();

      // Assert
      expect(interest).toBe(0);
      expect(premiumAccount.balance).toBe(-200); // No change
    });

    it('should manage premium features', () => {
      // Initial features
      expect(premiumAccount.hasFeature('overdraft')).toBe(true);
      expect(premiumAccount.hasFeature('priority_support')).toBe(true);
      expect(premiumAccount.hasFeature('monthly_statements')).toBe(true);

      // Add new feature
      premiumAccount.upgradeFeature('investment_advice');
      expect(premiumAccount.hasFeature('investment_advice')).toBe(true);

      // Verify feature list
      const features = premiumAccount.premiumFeatures;
      expect(features).toContain('overdraft');
      expect(features).toContain('investment_advice');

      // Verify logging and audit
      expect(mockLogger.log).toHaveBeenCalledWith(
        'Feature enabled: investment_advice',
        'info'
      );
      expect(mockAuditService.recordEvent).toHaveBeenCalledWith({
        accountNumber: 'PREM123',
        action: 'feature_upgraded',
        timestamp: expect.any(Date),
        details: { feature: 'investment_advice' }
      });
    });

    it('should reject duplicate feature upgrades', () => {
      expect(() => premiumAccount.upgradeFeature('overdraft'))
        .toThrow('Feature overdraft already enabled');
    });

    it('should calculate available balance including overdraft', () => {
      expect(premiumAccount.getAvailableBalance()).toBe(1500); // 1000 + 500 overdraft

      premiumAccount.withdraw(800);
      expect(premiumAccount.getAvailableBalance()).toBe(700); // 200 + 500 overdraft
    });

    it('should track overdraft usage', () => {
      expect(premiumAccount.getOverdraftUsed()).toBe(0);

      premiumAccount.withdraw(1200);
      expect(premiumAccount.getOverdraftUsed()).toBe(200);

      premiumAccount.deposit(300);
      expect(premiumAccount.getOverdraftUsed()).toBe(0); // Back to positive balance
    });
  });

  // βœ… Testing inherited behavior still works
  describe('Inherited behavior', () => {
    it('should still support basic account operations', () => {
      // Deposit
      premiumAccount.deposit(500);
      expect(premiumAccount.balance).toBe(1500);

      // Basic withdrawal (without overdraft)
      premiumAccount.withdraw(200);
      expect(premiumAccount.balance).toBe(1300);

      // Transfer
      const targetAccount = new BankAccount(100);
      premiumAccount.transfer(300, targetAccount);
      expect(premiumAccount.balance).toBe(1000);
      expect(targetAccount.balance).toBe(400);
    });

    it('should maintain transaction history', () => {
      premiumAccount.deposit(200);
      premiumAccount.withdraw(100);

      const history = premiumAccount.transactionHistory;
      expect(history).toHaveLength(2);
      expect(history[0].type).toBe('deposit');
      expect(history[1].type).toBe('withdrawal');
    });

    it('should respect account locking', () => {
      premiumAccount.lockAccount();
      
      expect(() => premiumAccount.deposit(100))
        .toThrow('Account is locked');
      
      expect(() => premiumAccount.withdraw(100))
        .toThrow('Account is locked');
    });
  });
});

πŸ”§ Testing Dependency Injection and Mocking

πŸ’‰ Advanced Dependency Testing

Complex classes often depend on external services, requiring sophisticated mocking strategies.

// 🌟 Comprehensive dependency injection testing

describe('Dependency Injection Testing', () => {
  let mockLogger: jest.Mocked<Logger>;
  let mockNotificationService: jest.Mocked<NotificationService>;
  let mockAuditService: jest.Mocked<AuditService>;

  beforeEach(() => {
    // Create detailed mocks
    mockLogger = {
      log: jest.fn(),
      getLastLogMessage: jest.fn().mockReturnValue(null)
    };

    mockNotificationService = {
      sendNotification: jest.fn(),
      getNotificationHistory: jest.fn().mockReturnValue([])
    };

    mockAuditService = {
      recordEvent: jest.fn(),
      getAuditTrail: jest.fn().mockReturnValue([])
    };
  });

  // βœ… Testing constructor dependency injection
  describe('Constructor dependency injection', () => {
    it('should inject dependencies correctly', () => {
      // Act
      const account = new PremiumBankAccount(
        1000,
        'TEST123',
        mockLogger,
        mockNotificationService,
        mockAuditService
      );

      // Trigger behavior that uses dependencies
      account.deposit(100);

      // Assert dependencies were called
      expect(mockLogger.log).toHaveBeenCalled();
      expect(mockAuditService.recordEvent).toHaveBeenCalled();
    });

    it('should handle null/undefined dependencies gracefully', () => {
      // This would require the class to handle null dependencies
      // In TypeScript, we'd typically use optional dependencies or null objects
      
      expect(() => {
        new PremiumBankAccount(
          1000,
          'TEST123',
          null as any,
          mockNotificationService,
          mockAuditService
        );
      }).toThrow(); // Or handle gracefully depending on implementation
    });
  });

  // βœ… Testing mock behavior configuration
  describe('Mock behavior configuration', () => {
    let account: PremiumBankAccount;

    beforeEach(() => {
      account = new PremiumBankAccount(
        1000,
        'TEST123',
        mockLogger,
        mockNotificationService,
        mockAuditService
      );
    });

    it('should configure mocks with specific return values', () => {
      // Arrange
      mockLogger.getLastLogMessage.mockReturnValue('Last log message');
      mockNotificationService.getNotificationHistory.mockReturnValue([
        'Notification 1',
        'Notification 2'
      ]);
      mockAuditService.getAuditTrail.mockReturnValue([
        {
          accountNumber: 'TEST123',
          action: 'test_action',
          timestamp: new Date(),
          details: {}
        }
      ]);

      // Act & Assert
      expect(mockLogger.getLastLogMessage()).toBe('Last log message');
      expect(mockNotificationService.getNotificationHistory('TEST123')).toEqual([
        'Notification 1',
        'Notification 2'
      ]);
      expect(mockAuditService.getAuditTrail('TEST123')).toHaveLength(1);
    });

    it('should test async mock behaviors', async () => {
      // Arrange
      mockNotificationService.sendNotification.mockResolvedValue();

      // Act
      account.withdraw(1500); // Should trigger notification

      // Wait for async operation
      await new Promise(resolve => setTimeout(resolve, 0));

      // Assert
      expect(mockNotificationService.sendNotification).toHaveBeenCalledWith(
        'TEST123',
        'Large withdrawal of 1500 processed'
      );
    });

    it('should test mock failures', async () => {
      // Arrange
      const notificationError = new Error('Notification service down');
      mockNotificationService.sendNotification.mockRejectedValue(notificationError);

      // Act
      account.withdraw(1500);

      // Wait for async operation and error handling
      await new Promise(resolve => setTimeout(resolve, 10));

      // Assert
      expect(mockLogger.log).toHaveBeenCalledWith(
        'Failed to send notification: Notification service down',
        'error'
      );
    });

    it('should verify mock call parameters', () => {
      // Act
      account.withdraw(250, 'ATM withdrawal');

      // Assert detailed call verification
      expect(mockAuditService.recordEvent).toHaveBeenCalledWith({
        accountNumber: 'TEST123',
        action: 'withdrawal',
        timestamp: expect.any(Date),
        details: {
          amount: 250,
          description: 'ATM withdrawal',
          oldBalance: 1000,
          newBalance: 750
        }
      });

      // Verify call count
      expect(mockAuditService.recordEvent).toHaveBeenCalledTimes(2); // Constructor + withdrawal
    });

    it('should test mock call order', () => {
      // Act
      account.withdraw(300);
      account.deposit(200);

      // Assert call order
      const auditCalls = mockAuditService.recordEvent.mock.calls;
      expect(auditCalls[1][0].action).toBe('withdrawal'); // Second call (after constructor)
      expect(auditCalls[2][0].action).toBe('interest_applied'); // If deposit triggers interest
    });
  });

  // βœ… Testing mock verification patterns
  describe('Mock verification patterns', () => {
    let account: PremiumBankAccount;

    beforeEach(() => {
      account = new PremiumBankAccount(
        1000,
        'TEST123',
        mockLogger,
        mockNotificationService,
        mockAuditService
      );
      
      // Clear constructor calls
      jest.clearAllMocks();
    });

    it('should verify no unexpected calls', () => {
      // Act - perform operation that shouldn't trigger notifications
      account.deposit(100);

      // Assert
      expect(mockNotificationService.sendNotification).not.toHaveBeenCalled();
    });

    it('should verify exact number of calls', () => {
      // Act
      account.withdraw(200);
      account.withdraw(300);
      account.withdraw(100);

      // Assert
      expect(mockAuditService.recordEvent).toHaveBeenCalledTimes(3);
      expect(mockLogger.log).toHaveBeenCalledTimes(3);
    });

    it('should verify calls with specific matchers', () => {
      // Act
      account.upgradeFeature('new_feature');

      // Assert with specific matchers
      expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          accountNumber: 'TEST123',
          action: 'feature_upgraded',
          timestamp: expect.any(Date),
          details: expect.objectContaining({
            feature: 'new_feature'
          })
        })
      );
    });

    it('should verify partial object matching', () => {
      // Act
      const interest = account.applyMonthlyInterest();

      // Assert with partial matching
      expect(mockAuditService.recordEvent).toHaveBeenCalledWith(
        expect.objectContaining({
          action: 'interest_applied',
          details: expect.objectContaining({
            rate: 0.02 // Default rate
          })
        })
      );
    });

    it('should verify mock implementation was called', () => {
      // Arrange - set up mock implementation
      const mockImplementation = jest.fn();
      mockLogger.log.mockImplementation(mockImplementation);

      // Act
      account.deposit(100);

      // Assert
      expect(mockImplementation).toHaveBeenCalled();
    });
  });

  // βœ… Testing spy patterns
  describe('Spy patterns', () => {
    it('should spy on real object methods', () => {
      // Create a real logger instance
      const realLogger: Logger = {
        log: (message: string, level?: string) => {
          console.log(`[${level?.toUpperCase() || 'INFO'}] ${message}`);
        },
        getLastLogMessage: () => 'Real last message'
      };

      // Spy on the real object
      const loggerSpy = jest.spyOn(realLogger, 'log');
      const getLastMessageSpy = jest.spyOn(realLogger, 'getLastLogMessage');

      // Create account with real logger
      const account = new PremiumBankAccount(
        1000,
        'SPY123',
        realLogger,
        mockNotificationService,
        mockAuditService
      );

      // Act
      account.deposit(200);

      // Assert
      expect(loggerSpy).toHaveBeenCalled();
      expect(getLastMessageSpy).not.toHaveBeenCalled(); // Wasn't called in deposit

      // Restore spies
      loggerSpy.mockRestore();
      getLastMessageSpy.mockRestore();
    });

    it('should spy on class methods', () => {
      const account = new PremiumBankAccount(
        1000,
        'SPY123',
        mockLogger,
        mockNotificationService,
        mockAuditService
      );

      // Spy on class methods
      const calculateInterestSpy = jest.spyOn(account, 'calculateInterest');
      const depositSpy = jest.spyOn(account, 'deposit');

      // Act
      account.applyMonthlyInterest();

      // Assert
      expect(calculateInterestSpy).toHaveBeenCalledWith(0.02, 30);
      expect(depositSpy).toHaveBeenCalled();

      // Restore spies
      calculateInterestSpy.mockRestore();
      depositSpy.mockRestore();
    });
  });

  // βœ… Testing mock factories and builders
  describe('Mock factories', () => {
    // Factory for creating configured mocks
    const createMockLogger = (config: {
      shouldSucceed?: boolean;
      lastMessage?: string;
    } = {}): jest.Mocked<Logger> => {
      const mock = {
        log: jest.fn(),
        getLastLogMessage: jest.fn()
      };

      if (config.shouldSucceed === false) {
        mock.log.mockImplementation(() => {
          throw new Error('Logger error');
        });
      }

      if (config.lastMessage) {
        mock.getLastLogMessage.mockReturnValue(config.lastMessage);
      }

      return mock;
    };

    const createMockNotificationService = (config: {
      shouldSucceed?: boolean;
      history?: string[];
    } = {}): jest.Mocked<NotificationService> => {
      const mock = {
        sendNotification: jest.fn(),
        getNotificationHistory: jest.fn()
      };

      if (config.shouldSucceed === false) {
        mock.sendNotification.mockRejectedValue(new Error('Notification failed'));
      } else {
        mock.sendNotification.mockResolvedValue();
      }

      mock.getNotificationHistory.mockReturnValue(config.history || []);

      return mock;
    };

    it('should use mock factory for success scenario', () => {
      // Arrange
      const successLogger = createMockLogger({ shouldSucceed: true });
      const successNotifications = createMockNotificationService({ shouldSucceed: true });

      const account = new PremiumBankAccount(
        1000,
        'FACTORY123',
        successLogger,
        successNotifications,
        mockAuditService
      );

      // Act
      account.withdraw(1500);

      // Assert
      expect(successLogger.log).toHaveBeenCalled();
      expect(successNotifications.sendNotification).toHaveBeenCalled();
    });

    it('should use mock factory for failure scenario', async () => {
      // Arrange
      const failingLogger = createMockLogger({ shouldSucceed: false });
      const failingNotifications = createMockNotificationService({ shouldSucceed: false });

      const account = new PremiumBankAccount(
        1000,
        'FACTORY456',
        failingLogger,
        failingNotifications,
        mockAuditService
      );

      // Act & Assert
      expect(() => account.deposit(100)).toThrow('Logger error');
    });
  });
});

πŸ§ͺ Testing Private Methods and Encapsulation

πŸ”’ Testing Through Public Interface

Testing private methods requires careful consideration of encapsulation while ensuring comprehensive coverage.

// 🌟 Testing private methods and encapsulation

describe('Private Method Testing', () => {
  let account: BankAccount;

  beforeEach(() => {
    account = new BankAccount(1000, 'TEST123');
  });

  // βœ… Testing private methods through public interface
  describe('Testing private validation methods', () => {
    it('should test validateAmount through public methods', () => {
      // Test positive amounts work
      expect(() => account.deposit(100)).not.toThrow();
      expect(() => account.withdraw(100)).not.toThrow();

      // Test zero amount validation
      expect(() => account.deposit(0))
        .toThrow('Amount must be positive');
      expect(() => account.withdraw(0))
        .toThrow('Amount must be positive');

      // Test negative amount validation
      expect(() => account.deposit(-50))
        .toThrow('Amount must be positive');
      expect(() => account.withdraw(-50))
        .toThrow('Amount must be positive');

      // Test invalid number validation
      expect(() => account.deposit(NaN))
        .toThrow('Amount must be a valid number');
      expect(() => account.withdraw(Infinity))
        .toThrow('Amount must be a valid number');
    });

    it('should test checkAccountLocked through public methods', () => {
      // Account starts unlocked - operations should work
      expect(() => account.deposit(100)).not.toThrow();
      expect(() => account.withdraw(100)).not.toThrow();

      // Lock account
      account.lockAccount();

      // Now operations should fail
      expect(() => account.deposit(100))
        .toThrow('Account is locked');
      expect(() => account.withdraw(100))
        .toThrow('Account is locked');

      // Unlock and verify operations work again
      account.unlockAccount();
      expect(() => account.deposit(100)).not.toThrow();
    });

    it('should test checkSufficientFunds through withdrawal', () => {
      // Should work with sufficient funds
      expect(() => account.withdraw(500)).not.toThrow();
      expect(account.balance).toBe(500);

      // Should fail with insufficient funds
      expect(() => account.withdraw(600))
        .toThrow('Insufficient funds');
      
      // Balance should remain unchanged
      expect(account.balance).toBe(500);
    });

    it('should test checkDailyLimit through multiple withdrawals', () => {
      // First withdrawal should work
      account.withdraw(800);
      expect(account.balance).toBe(200);

      // Second withdrawal should work (within daily limit)
      account.withdraw(100);
      expect(account.balance).toBe(100);

      // Third withdrawal should fail (exceeds daily limit)
      expect(() => account.withdraw(200))
        .toThrow('Daily withdrawal limit exceeded');
    });
  });

  // βœ… Testing private helper methods through side effects
  describe('Testing private helper methods', () => {
    it('should test generateAccountNumber uniqueness', () => {
      // Create multiple accounts and verify unique account numbers
      const accounts = Array.from({ length: 100 }, () => new BankAccount());
      const accountNumbers = accounts.map(acc => acc.accountNumber);
      const uniqueNumbers = new Set(accountNumbers);

      expect(uniqueNumbers.size).toBe(accountNumbers.length);
    });

    it('should test generateTransactionId uniqueness', () => {
      // Perform multiple transactions and verify unique IDs
      for (let i = 0; i < 10; i++) {
        account.deposit(10, `Deposit ${i}`);
      }

      const transactions = account.transactionHistory;
      const transactionIds = transactions.map(tx => tx.id);
      const uniqueIds = new Set(transactionIds);

      expect(uniqueIds.size).toBe(transactionIds.length);
      
      // Verify ID format
      transactionIds.forEach(id => {
        expect(id).toMatch(/^TXN-\d+-[a-z0-9]{6}$/);
      });
    });

    it('should test recordTransaction through transaction history', () => {
      // Initial state
      expect(account.transactionHistory).toHaveLength(0);

      // Perform operations
      account.deposit(500, 'Test deposit');
      account.withdraw(200, 'Test withdrawal');

      // Verify transactions were recorded correctly
      const history = account.transactionHistory;
      expect(history).toHaveLength(2);

      // Verify first transaction
      expect(history[0]).toMatchObject({
        type: 'deposit',
        amount: 500,
        description: 'Test deposit',
        balanceBefore: 1000,
        balanceAfter: 1500
      });

      // Verify second transaction
      expect(history[1]).toMatchObject({
        type: 'withdrawal',
        amount: 200,
        description: 'Test withdrawal',
        balanceBefore: 1500,
        balanceAfter: 1300
      });

      // Verify all required fields are present
      history.forEach(tx => {
        expect(tx.id).toBeDefined();
        expect(tx.timestamp).toBeInstanceOf(Date);
      });
    });
  });

  // βœ… Testing state consistency with private methods
  describe('Testing state consistency', () => {
    it('should maintain consistent internal state', () => {
      // Perform various operations
      account.deposit(500);           // Balance: 1500
      account.withdraw(200);          // Balance: 1300
      account.deposit(100);           // Balance: 1400
      account.withdraw(300);          // Balance: 1100

      // Verify final state consistency
      expect(account.balance).toBe(1100);
      expect(account.transactionHistory).toHaveLength(4);
      expect(account.dailyWithdrawalsRemaining).toBe(500); // 1000 - 500 withdrawn

      // Verify transaction balance consistency
      const transactions = account.transactionHistory;
      let expectedBalance = 1000; // Initial balance

      transactions.forEach(tx => {
        expect(tx.balanceBefore).toBe(expectedBalance);
        
        if (tx.type === 'deposit') {
          expectedBalance += tx.amount;
        } else {
          expectedBalance -= tx.amount;
        }
        
        expect(tx.balanceAfter).toBe(expectedBalance);
      });

      expect(expectedBalance).toBe(account.balance);
    });

    it('should handle edge cases in state management', () => {
      // Test boundary conditions
      account.withdraw(1000); // Withdraw entire balance
      expect(account.balance).toBe(0);

      // Should still be able to deposit
      account.deposit(1);
      expect(account.balance).toBe(1);

      // Should not be able to withdraw more than balance
      expect(() => account.withdraw(2))
        .toThrow('Insufficient funds');
    });
  });

  // βœ… Testing private method interactions
  describe('Private method interactions', () => {
    it('should test interaction between validation methods', () => {
      // Lock account
      account.lockAccount();

      // All validation should fail on locked account
      expect(() => account.deposit(100))
        .toThrow('Account is locked');
      
      expect(() => account.withdraw(100))
        .toThrow('Account is locked');

      // Unlock and test amount validation
      account.unlockAccount();
      
      expect(() => account.deposit(-100))
        .toThrow('Amount must be positive');
      
      expect(() => account.withdraw(0))
        .toThrow('Amount must be positive');
    });

    it('should test validation order in withdrawal', () => {
      // Set up scenario where multiple validations could fail
      account.setDailyWithdrawalLimit(500);
      account.withdraw(400); // Use up most daily limit

      // Test that amount validation happens first
      expect(() => account.withdraw(-100))
        .toThrow('Amount must be positive'); // Not daily limit error

      // Test that account lock validation happens next
      account.lockAccount();
      expect(() => account.withdraw(50))
        .toThrow('Account is locked'); // Not insufficient funds or daily limit

      // Test sufficient funds validation
      account.unlockAccount();
      expect(() => account.withdraw(700))
        .toThrow('Insufficient funds'); // Not daily limit (would be checked after)

      // Test daily limit validation
      expect(() => account.withdraw(200))
        .toThrow('Daily withdrawal limit exceeded');
    });
  });
});

// βœ… Testing encapsulation boundaries
describe('Encapsulation Testing', () => {
  let account: BankAccount;

  beforeEach(() => {
    account = new BankAccount(1000, 'TEST123');
  });

  it('should not expose internal state directly', () => {
    // Verify private fields are not accessible
    expect((account as any)._balance).toBeUndefined();
    expect((account as any)._transactions).toBeUndefined();
    expect((account as any)._accountNumber).toBeUndefined();

    // Access should only be through public interface
    expect(account.balance).toBe(1000);
    expect(account.accountNumber).toBe('TEST123');
    expect(account.transactionHistory).toBeDefined();
  });

  it('should return immutable copies of internal data', () => {
    // Add some transactions
    account.deposit(100);
    account.withdraw(50);

    // Get transaction history
    const history1 = account.transactionHistory;
    const history2 = account.transactionHistory;

    // Should be different array instances
    expect(history1).not.toBe(history2);
    
    // But with same content
    expect(history1).toEqual(history2);

    // Modifying returned array shouldn't affect internal state
    const originalLength = history1.length;
    
    // This should not affect the account's internal state
    expect(() => {
      (history1 as any).push({ fake: 'transaction' });
    }).toThrow(); // Should be immutable

    // Verify internal state unchanged
    expect(account.transactionHistory).toHaveLength(originalLength);
  });

  it('should control access through methods only', () => {
    // Balance should only change through deposit/withdraw methods
    const initialBalance = account.balance;
    
    // Cannot directly modify balance
    expect(() => {
      (account as any).balance = 5000;
    }).not.toThrow(); // Assignment might not throw, but shouldn't change internal state
    
    // Public balance should still reflect internal state
    expect(account.balance).toBe(initialBalance);

    // Only way to change balance is through public methods
    account.deposit(500);
    expect(account.balance).toBe(initialBalance + 500);
  });

  it('should validate all inputs at public interface', () => {
    // Every public method should validate its inputs
    const invalidInputs = [NaN, Infinity, -1, 0, null, undefined];
    
    invalidInputs.forEach(invalidInput => {
      if (typeof invalidInput === 'number') {
        expect(() => account.deposit(invalidInput))
          .toThrow();
        expect(() => account.withdraw(invalidInput))
          .toThrow();
      }
    });
  });
});

🎯 Advanced Testing Patterns

πŸ† Production-Ready Class Testing

Here are advanced patterns for comprehensive class testing in production applications.

// 🌟 Advanced class testing patterns

describe('Advanced Class Testing Patterns', () => {
  // βœ… Testing class state machines
  describe('State Machine Testing', () => {
    enum AccountState {
      ACTIVE = 'active',
      LOCKED = 'locked',
      SUSPENDED = 'suspended',
      CLOSED = 'closed'
    }

    class StatefulBankAccount extends BankAccount {
      private _state: AccountState = AccountState.ACTIVE;

      get state(): AccountState {
        return this._state;
      }

      setState(newState: AccountState): void {
        if (!this.isValidStateTransition(this._state, newState)) {
          throw new Error(`Invalid state transition from ${this._state} to ${newState}`);
        }
        this._state = newState;
      }

      deposit(amount: number, description?: string): void {
        if (this._state !== AccountState.ACTIVE) {
          throw new Error(`Cannot deposit: account is ${this._state}`);
        }
        super.deposit(amount, description);
      }

      withdraw(amount: number, description?: string): void {
        if (this._state !== AccountState.ACTIVE) {
          throw new Error(`Cannot withdraw: account is ${this._state}`);
        }
        super.withdraw(amount, description);
      }

      private isValidStateTransition(from: AccountState, to: AccountState): boolean {
        const validTransitions: Record<AccountState, AccountState[]> = {
          [AccountState.ACTIVE]: [AccountState.LOCKED, AccountState.SUSPENDED, AccountState.CLOSED],
          [AccountState.LOCKED]: [AccountState.ACTIVE, AccountState.SUSPENDED, AccountState.CLOSED],
          [AccountState.SUSPENDED]: [AccountState.ACTIVE, AccountState.CLOSED],
          [AccountState.CLOSED]: [] // No transitions from closed state
        };

        return validTransitions[from].includes(to);
      }
    }

    let statefulAccount: StatefulBankAccount;

    beforeEach(() => {
      statefulAccount = new StatefulBankAccount(1000, 'STATE123');
    });

    it('should start in active state', () => {
      expect(statefulAccount.state).toBe(AccountState.ACTIVE);
    });

    it('should allow valid state transitions', () => {
      // Active -> Locked
      statefulAccount.setState(AccountState.LOCKED);
      expect(statefulAccount.state).toBe(AccountState.LOCKED);

      // Locked -> Active
      statefulAccount.setState(AccountState.ACTIVE);
      expect(statefulAccount.state).toBe(AccountState.ACTIVE);

      // Active -> Suspended
      statefulAccount.setState(AccountState.SUSPENDED);
      expect(statefulAccount.state).toBe(AccountState.SUSPENDED);

      // Suspended -> Closed
      statefulAccount.setState(AccountState.CLOSED);
      expect(statefulAccount.state).toBe(AccountState.CLOSED);
    });

    it('should reject invalid state transitions', () => {
      // Close account
      statefulAccount.setState(AccountState.CLOSED);

      // Should not be able to transition from closed state
      expect(() => statefulAccount.setState(AccountState.ACTIVE))
        .toThrow('Invalid state transition from closed to active');

      expect(() => statefulAccount.setState(AccountState.LOCKED))
        .toThrow('Invalid state transition from closed to locked');
    });

    it('should restrict operations based on state', () => {
      // Lock account
      statefulAccount.setState(AccountState.LOCKED);

      // Operations should be rejected
      expect(() => statefulAccount.deposit(100))
        .toThrow('Cannot deposit: account is locked');

      expect(() => statefulAccount.withdraw(100))
        .toThrow('Cannot withdraw: account is locked');

      // Unlock and verify operations work
      statefulAccount.setState(AccountState.ACTIVE);
      expect(() => statefulAccount.deposit(100)).not.toThrow();
      expect(() => statefulAccount.withdraw(100)).not.toThrow();
    });

    it('should test all possible state transitions', () => {
      const allStates = Object.values(AccountState);
      const testedTransitions: Array<[AccountState, AccountState]> = [];

      // Test all valid transitions
      allStates.forEach(fromState => {
        allStates.forEach(toState => {
          if (fromState !== toState) {
            // Reset to fromState
            statefulAccount = new StatefulBankAccount(1000, 'STATE123');
            if (fromState !== AccountState.ACTIVE) {
              // Navigate to fromState (simplified for test)
              try {
                statefulAccount.setState(fromState);
              } catch {
                // Some states might not be reachable from ACTIVE
                return;
              }
            }

            // Try transition
            try {
              statefulAccount.setState(toState);
              testedTransitions.push([fromState, toState]);
            } catch (error) {
              // Invalid transition - that's expected for some combinations
            }
          }
        });
      });

      // Verify we tested the expected valid transitions
      expect(testedTransitions).toContainEqual([AccountState.ACTIVE, AccountState.LOCKED]);
      expect(testedTransitions).toContainEqual([AccountState.ACTIVE, AccountState.SUSPENDED]);
      expect(testedTransitions).toContainEqual([AccountState.LOCKED, AccountState.ACTIVE]);
    });
  });

  // βœ… Testing performance characteristics
  describe('Performance Testing', () => {
    it('should handle large transaction volumes efficiently', () => {
      const account = new BankAccount(10000);
      const transactionCount = 1000;

      const startTime = Date.now();

      // Perform many transactions
      for (let i = 0; i < transactionCount; i++) {
        if (i % 2 === 0) {
          account.deposit(10, `Deposit ${i}`);
        } else {
          account.withdraw(5, `Withdrawal ${i}`);
        }
      }

      const endTime = Date.now();
      const duration = endTime - startTime;

      // Should complete within reasonable time
      expect(duration).toBeLessThan(1000); // 1 second

      // Verify all transactions were recorded
      expect(account.transactionHistory).toHaveLength(transactionCount);

      // Verify final balance is correct
      const expectedBalance = 10000 + (transactionCount / 2 * 10) - (transactionCount / 2 * 5);
      expect(account.balance).toBe(expectedBalance);
    });

    it('should handle memory efficiently with large transaction history', () => {
      const account = new BankAccount(1000);
      
      // Add many transactions
      for (let i = 0; i < 10000; i++) {
        account.deposit(1, `Transaction ${i}`);
      }

      // Memory usage should not be excessive
      const history = account.transactionHistory;
      expect(history).toHaveLength(10000);

      // Getting history multiple times should not accumulate memory
      const history1 = account.transactionHistory;
      const history2 = account.transactionHistory;
      
      expect(history1).toEqual(history2);
      expect(history1).not.toBe(history2); // Different instances
    });
  });

  // βœ… Testing error recovery
  describe('Error Recovery Testing', () => {
    let account: PremiumBankAccount;
    let mockLogger: jest.Mocked<Logger>;
    let mockNotificationService: jest.Mocked<NotificationService>;
    let mockAuditService: jest.Mocked<AuditService>;

    beforeEach(() => {
      mockLogger = {
        log: jest.fn(),
        getLastLogMessage: jest.fn()
      };

      mockNotificationService = {
        sendNotification: jest.fn(),
        getNotificationHistory: jest.fn()
      };

      mockAuditService = {
        recordEvent: jest.fn(),
        getAuditTrail: jest.fn()
      };

      account = new PremiumBankAccount(
        1000,
        'ERROR123',
        mockLogger,
        mockNotificationService,
        mockAuditService
      );
    });

    it('should handle logger failures gracefully', () => {
      // Arrange
      mockLogger.log.mockImplementation(() => {
        throw new Error('Logger is down');
      });

      // Act & Assert - operation should still succeed despite logger failure
      // (This depends on implementation - might need to catch logger errors)
      expect(() => account.deposit(100)).toThrow('Logger is down');
      
      // Alternative: if logger errors are caught internally
      // expect(() => account.deposit(100)).not.toThrow();
      // expect(account.balance).toBe(1100);
    });

    it('should handle notification failures gracefully', async () => {
      // Arrange
      mockNotificationService.sendNotification.mockRejectedValue(
        new Error('Notification service unavailable')
      );

      // Act
      account.withdraw(1500); // Should trigger notification

      // Wait for async notification attempt
      await new Promise(resolve => setTimeout(resolve, 10));

      // Assert - withdrawal should succeed despite notification failure
      expect(account.balance).toBe(-500);
      expect(mockLogger.log).toHaveBeenCalledWith(
        'Failed to send notification: Notification service unavailable',
        'error'
      );
    });

    it('should handle audit service failures', () => {
      // Arrange
      mockAuditService.recordEvent.mockImplementation(() => {
        throw new Error('Audit service is down');
      });

      // Act & Assert - depends on implementation
      // If audit is critical, operation should fail
      expect(() => account.withdraw(100)).toThrow('Audit service is down');
      
      // If audit failure is logged but operation continues:
      // expect(() => account.withdraw(100)).not.toThrow();
      // expect(mockLogger.log).toHaveBeenCalledWith(...);
    });

    it('should maintain consistency during partial failures', async () => {
      // Arrange - notification fails, but audit succeeds
      mockNotificationService.sendNotification.mockRejectedValue(new Error('Service down'));
      mockAuditService.recordEvent.mockImplementation(); // Succeeds

      // Act
      account.withdraw(1200);

      // Wait for async operations
      await new Promise(resolve => setTimeout(resolve, 10));

      // Assert - operation should complete with consistent state
      expect(account.balance).toBe(-200);
      expect(mockAuditService.recordEvent).toHaveBeenCalled();
      expect(mockLogger.log).toHaveBeenCalledWith(
        expect.stringContaining('Failed to send notification'),
        'error'
      );
    });
  });

  // βœ… Testing thread safety (if applicable)
  describe('Concurrency Testing', () => {
    it('should handle concurrent operations safely', async () => {
      const account = new BankAccount(1000);
      const operationCount = 100;
      const operations: Promise<void>[] = [];

      // Create many concurrent operations
      for (let i = 0; i < operationCount; i++) {
        if (i % 2 === 0) {
          operations.push(
            new Promise<void>(resolve => {
              setTimeout(() => {
                try {
                  account.deposit(10);
                } catch (error) {
                  // Handle any errors
                }
                resolve();
              }, Math.random() * 10);
            })
          );
        } else {
          operations.push(
            new Promise<void>(resolve => {
              setTimeout(() => {
                try {
                  account.withdraw(5);
                } catch (error) {
                  // Handle insufficient funds or other errors
                }
                resolve();
              }, Math.random() * 10);
            })
          );
        }
      }

      // Wait for all operations to complete
      await Promise.all(operations);

      // Verify final state is consistent
      const history = account.transactionHistory;
      expect(history.length).toBeGreaterThan(0);
      
      // Verify balance consistency with transaction history
      let calculatedBalance = 1000;
      history.forEach(tx => {
        if (tx.type === 'deposit') {
          calculatedBalance += tx.amount;
        } else {
          calculatedBalance -= tx.amount;
        }
      });

      expect(account.balance).toBe(calculatedBalance);
    });
  });

  // βœ… Testing integration scenarios
  describe('Integration Testing', () => {
    it('should test complex multi-account scenarios', () => {
      // Create multiple accounts
      const account1 = new BankAccount(1000, 'ACC001');
      const account2 = new BankAccount(500, 'ACC002');
      const account3 = new BankAccount(2000, 'ACC003');

      // Perform complex operations
      account1.transfer(300, account2, 'Payment to ACC002');
      account2.transfer(200, account3, 'Payment to ACC003');
      account3.transfer(500, account1, 'Refund to ACC001');

      // Verify final balances
      expect(account1.balance).toBe(1200); // 1000 - 300 + 500
      expect(account2.balance).toBe(600);  // 500 + 300 - 200
      expect(account3.balance).toBe(1700); // 2000 + 200 - 500

      // Verify transaction histories
      expect(account1.transactionHistory).toHaveLength(2); // 1 withdrawal, 1 deposit
      expect(account2.transactionHistory).toHaveLength(2); // 1 deposit, 1 withdrawal
      expect(account3.transactionHistory).toHaveLength(2); // 1 deposit, 1 withdrawal
    });

    it('should test business workflow scenarios', () => {
      const savingsAccount = new SavingsAccount('SAV001', 'John Doe', 0.05);
      
      // Simulate monthly activities
      for (let month = 1; month <= 12; month++) {
        // Monthly salary deposit
        savingsAccount.deposit(5000);
        
        // Monthly expenses
        savingsAccount.withdraw(3000);
        
        // Apply monthly interest
        savingsAccount.applyInterest();
      }

      // After 12 months, verify state
      expect(savingsAccount.getBalance()).toBeGreaterThan(24000); // Base amount + interest
      
      // Verify reasonable interest was earned
      const expectedMinBalance = 12 * (5000 - 3000); // 24000
      const actualBalance = savingsAccount.getBalance();
      const interestEarned = actualBalance - expectedMinBalance;
      
      expect(interestEarned).toBeGreaterThan(1000); // Should have earned significant interest
    });
  });
});

πŸŽ‰ Conclusion

Congratulations! You’ve mastered the art of testing TypeScript classes and object-oriented patterns! 🎯

πŸ”‘ Key Takeaways

  1. Constructor Testing: Verify proper initialization and parameter validation
  2. Property Testing: Test getters, setters, and state management
  3. Method Testing: Comprehensive testing of behavior and side effects
  4. Inheritance Testing: Verify parent/child class relationships and overrides
  5. Dependency Injection: Mock external dependencies effectively
  6. Encapsulation: Test through public interfaces while respecting boundaries
  7. State Management: Verify state transitions and consistency
  8. Error Handling: Test failure scenarios and recovery mechanisms

πŸš€ Next Steps

  • Testing React Components: Learn component testing with React Testing Library
  • Testing Hooks: Master custom React hook testing patterns
  • Integration Testing: Build comprehensive integration test suites
  • End-to-End Testing: Test complete user workflows with Cypress/Playwright
  • Testing APIs: Learn backend testing patterns for TypeScript services

You now have the skills to test any TypeScript class hierarchy with confidence, ensuring your object-oriented code is robust, maintainable, and bug-free! 🌟