+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 145 of 355

⚑ Testing Async Code: Promises and Callbacks

Master the art of testing asynchronous TypeScript code with promises, callbacks, and async/await patterns for reliable async testing πŸš€

πŸš€Intermediate
24 min read

Prerequisites

  • Understanding of TypeScript fundamentals and async programming πŸ“
  • Knowledge of promises, callbacks, and async/await patterns ⚑
  • Familiarity with Jest testing framework and unit testing πŸ’»

What you'll learn

  • Test asynchronous code with promises, callbacks, and async/await 🎯
  • Handle timing issues and race conditions in async tests πŸ—οΈ
  • Mock async dependencies and external services effectively πŸ›
  • Build reliable test suites for complex async workflows ✨

🎯 Introduction

Welcome to the async testing laboratory of TypeScript! ⚑ If testing synchronous code were like examining a photograph where everything is captured in a single moment, then testing asynchronous code would be like directing a complex movie scene where actions happen over time, actors enter and exit at different moments, and you need to coordinate everything perfectly to capture the story as it unfolds - except in our case, we’re ensuring our async operations work correctly across all timing scenarios!

Testing asynchronous code presents unique challenges: timing issues, race conditions, callback hell, and the need to wait for operations to complete. In TypeScript, we have the additional benefit of type safety for our async operations, but we still need to test that our promises resolve correctly, our callbacks are called with the right parameters, and our error handling works as expected.

By the end of this tutorial, you’ll be a master of async testing in TypeScript, capable of testing everything from simple promise chains to complex async workflows involving multiple services, timeouts, and error conditions. You’ll learn to handle timing issues, mock async dependencies, and create reliable test suites that give you confidence in your async code. Let’s dive into the world of bulletproof async testing! 🌟

πŸ“š Understanding Async Testing Fundamentals

πŸ€” Why Async Testing Is Different

Async testing requires special handling because operations don’t complete immediately, and we need to wait for results while handling potential errors and timeouts.

// 🌟 Setting up comprehensive async testing environment

// Types for our async testing examples
interface ApiResponse<T> {
  data: T;
  status: number;
  message: string;
  timestamp: Date;
}

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

interface ValidationError {
  field: string;
  message: string;
  code: string;
}

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>;
}

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>;
  validateEmail(email: string): Promise<boolean>;
}

interface CacheService {
  get<T>(key: string): Promise<T | null>;
  set<T>(key: string, value: T, ttl?: number): Promise<void>;
  delete(key: string): Promise<boolean>;
  clear(): Promise<void>;
}

// Service that performs async operations
class UserService {
  constructor(
    private db: DatabaseConnection,
    private emailService: EmailService,
    private cache: CacheService
  ) {}

  // Promise-based async method
  async createUser(userData: Omit<User, 'id' | 'createdAt'>): Promise<User> {
    // Validate email first
    const isValidEmail = await this.emailService.validateEmail(userData.email);
    if (!isValidEmail) {
      throw new Error('Invalid email address');
    }

    // Check if user exists
    const existingUsers = await this.db.query<User>(
      'SELECT * FROM users WHERE email = ?',
      [userData.email]
    );

    if (existingUsers.length > 0) {
      throw new Error('User already exists');
    }

    // Create user
    const user: User = {
      id: this.generateId(),
      ...userData,
      createdAt: new Date()
    };

    await this.db.query(
      'INSERT INTO users (id, name, email, isActive, createdAt) VALUES (?, ?, ?, ?, ?)',
      [user.id, user.name, user.email, user.isActive, user.createdAt]
    );

    // Cache the user
    await this.cache.set(`user:${user.id}`, user, 3600);

    // Send welcome email (don't wait for it)
    this.emailService.sendEmail(
      user.email,
      'Welcome!',
      `Welcome ${user.name}!`
    ).catch(error => {
      console.error('Failed to send welcome email:', error);
    });

    return user;
  }

  // Callback-based async method (legacy style)
  getUserById(id: string, callback: (error: Error | null, user?: User) => void): void {
    // Simulate async operation with setTimeout
    setTimeout(async () => {
      try {
        // Check cache first
        const cachedUser = await this.cache.get<User>(`user:${id}`);
        if (cachedUser) {
          callback(null, cachedUser);
          return;
        }

        // Query database
        const users = await this.db.query<User>(
          'SELECT * FROM users WHERE id = ?',
          [id]
        );

        if (users.length === 0) {
          callback(new Error('User not found'));
          return;
        }

        const user = users[0];
        
        // Cache for next time
        await this.cache.set(`user:${user.id}`, user, 3600);
        
        callback(null, user);
      } catch (error) {
        callback(error as Error);
      }
    }, 100);
  }

  // Promise-based method with timeout
  async getUserWithTimeout(id: string, timeoutMs: number = 5000): Promise<User> {
    return new Promise((resolve, reject) => {
      const timeout = setTimeout(() => {
        reject(new Error('Operation timed out'));
      }, timeoutMs);

      this.getUserById(id, (error, user) => {
        clearTimeout(timeout);
        if (error) {
          reject(error);
        } else if (user) {
          resolve(user);
        } else {
          reject(new Error('Unknown error'));
        }
      });
    });
  }

  // Complex async workflow with multiple steps
  async processUserRegistration(userData: Omit<User, 'id' | 'createdAt'>): Promise<{
    user: User;
    emailSent: boolean;
    cacheUpdated: boolean;
  }> {
    let user: User;
    let emailSent = false;
    let cacheUpdated = false;

    // Use transaction for database operations
    await this.db.transaction(async (tx) => {
      // Create user
      user = {
        id: this.generateId(),
        ...userData,
        createdAt: new Date()
      };

      await tx.query(
        'INSERT INTO users (id, name, email, isActive, createdAt) VALUES (?, ?, ?, ?, ?)',
        [user.id, user.name, user.email, user.isActive, user.createdAt]
      );

      // Log registration
      await tx.query(
        'INSERT INTO user_activity (user_id, action, timestamp) VALUES (?, ?, ?)',
        [user.id, 'registration', new Date()]
      );
    });

    // Parallel operations after transaction
    const [cacheResult, emailResult] = await Promise.allSettled([
      this.cache.set(`user:${user!.id}`, user!, 3600),
      this.emailService.sendEmail(
        user!.email,
        'Welcome!',
        `Welcome ${user!.name}!`
      )
    ]);

    cacheUpdated = cacheResult.status === 'fulfilled';
    emailSent = emailResult.status === 'fulfilled';

    return {
      user: user!,
      emailSent,
      cacheUpdated
    };
  }

  private generateId(): string {
    return Math.random().toString(36).substr(2, 9);
  }
}

// Utility class for handling async operations
class AsyncOperationManager {
  private operations: Map<string, Promise<any>> = new Map();

  // Register an async operation
  registerOperation<T>(key: string, operation: Promise<T>): Promise<T> {
    this.operations.set(key, operation);
    
    // Clean up when done
    operation.finally(() => {
      this.operations.delete(key);
    });

    return operation;
  }

  // Wait for all operations to complete
  async waitForAll(): Promise<void> {
    await Promise.allSettled(Array.from(this.operations.values()));
  }

  // Cancel all pending operations (if they support cancellation)
  cancelAll(): void {
    this.operations.clear();
  }

  // Get status of operations
  getStatus(): {
    pending: number;
    keys: string[];
  } {
    return {
      pending: this.operations.size,
      keys: Array.from(this.operations.keys())
    };
  }
}

// API client for external service calls
class ApiClient {
  constructor(private baseUrl: string) {}

  async get<T>(endpoint: string): Promise<ApiResponse<T>> {
    const response = await fetch(`${this.baseUrl}${endpoint}`);
    const data = await response.json();
    
    return {
      data,
      status: response.status,
      message: response.statusText,
      timestamp: new Date()
    };
  }

  async post<T, R>(endpoint: string, data: T): Promise<ApiResponse<R>> {
    const response = await fetch(`${this.baseUrl}${endpoint}`, {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json'
      },
      body: JSON.stringify(data)
    });
    
    const responseData = await response.json();
    
    return {
      data: responseData,
      status: response.status,
      message: response.statusText,
      timestamp: new Date()
    };
  }

  // Method with retry logic
  async getWithRetry<T>(
    endpoint: string, 
    maxRetries: number = 3,
    delay: number = 1000
  ): Promise<ApiResponse<T>> {
    let lastError: Error;

    for (let attempt = 0; attempt <= maxRetries; attempt++) {
      try {
        return await this.get<T>(endpoint);
      } catch (error) {
        lastError = error as Error;
        
        if (attempt === maxRetries) {
          throw lastError;
        }

        // Wait before retry
        await new Promise(resolve => setTimeout(resolve, delay * (attempt + 1)));
      }
    }

    throw lastError!;
  }
}

πŸ§ͺ Testing Promise-Based Async Code

βœ… Basic Promise Testing

The most straightforward way to test promises is using async/await in your test functions.

// 🌟 Comprehensive promise testing patterns

describe('UserService - Promise Testing', () => {
  let userService: UserService;
  let mockDb: jest.Mocked<DatabaseConnection>;
  let mockEmailService: jest.Mocked<EmailService>;
  let mockCache: jest.Mocked<CacheService>;

  beforeEach(() => {
    // Create mocks
    mockDb = {
      connect: jest.fn(),
      disconnect: jest.fn(),
      query: jest.fn(),
      transaction: jest.fn()
    };

    mockEmailService = {
      sendEmail: jest.fn(),
      validateEmail: jest.fn()
    };

    mockCache = {
      get: jest.fn(),
      set: jest.fn(),
      delete: jest.fn(),
      clear: jest.fn()
    };

    userService = new UserService(mockDb, mockEmailService, mockCache);
  });

  // βœ… Test successful promise resolution
  describe('createUser', () => {
    it('should create user successfully when all validations pass', async () => {
      // Arrange
      const userData = {
        name: 'John Doe',
        email: '[email protected]',
        isActive: true
      };

      mockEmailService.validateEmail.mockResolvedValue(true);
      mockDb.query
        .mockResolvedValueOnce([]) // No existing users
        .mockResolvedValueOnce([]); // Insert successful
      mockCache.set.mockResolvedValue();
      mockEmailService.sendEmail.mockResolvedValue();

      // Act
      const result = await userService.createUser(userData);

      // Assert
      expect(result).toMatchObject({
        name: userData.name,
        email: userData.email,
        isActive: userData.isActive
      });
      expect(result.id).toBeDefined();
      expect(result.createdAt).toBeInstanceOf(Date);

      // Verify all async operations were called
      expect(mockEmailService.validateEmail).toHaveBeenCalledWith(userData.email);
      expect(mockDb.query).toHaveBeenCalledTimes(2);
      expect(mockCache.set).toHaveBeenCalledWith(
        `user:${result.id}`,
        result,
        3600
      );
    });

    // βœ… Test promise rejection with validation error
    it('should reject when email validation fails', async () => {
      // Arrange
      const userData = {
        name: 'John Doe',
        email: 'invalid-email',
        isActive: true
      };

      mockEmailService.validateEmail.mockResolvedValue(false);

      // Act & Assert
      await expect(userService.createUser(userData))
        .rejects
        .toThrow('Invalid email address');

      // Verify no database operations occurred
      expect(mockDb.query).not.toHaveBeenCalled();
      expect(mockCache.set).not.toHaveBeenCalled();
    });

    // βœ… Test promise rejection with database error
    it('should reject when database operation fails', async () => {
      // Arrange
      const userData = {
        name: 'John Doe',
        email: '[email protected]',
        isActive: true
      };

      mockEmailService.validateEmail.mockResolvedValue(true);
      mockDb.query.mockRejectedValue(new Error('Database connection failed'));

      // Act & Assert
      await expect(userService.createUser(userData))
        .rejects
        .toThrow('Database connection failed');

      // Verify email service was called but cache was not
      expect(mockEmailService.validateEmail).toHaveBeenCalled();
      expect(mockCache.set).not.toHaveBeenCalled();
    });

    // βœ… Test promise rejection with existing user
    it('should reject when user already exists', async () => {
      // Arrange
      const userData = {
        name: 'John Doe',
        email: '[email protected]',
        isActive: true
      };

      const existingUser = {
        id: 'existing-id',
        ...userData,
        createdAt: new Date()
      };

      mockEmailService.validateEmail.mockResolvedValue(true);
      mockDb.query.mockResolvedValue([existingUser]);

      // Act & Assert
      await expect(userService.createUser(userData))
        .rejects
        .toThrow('User already exists');

      // Verify database was queried but no insert occurred
      expect(mockDb.query).toHaveBeenCalledTimes(1);
      expect(mockCache.set).not.toHaveBeenCalled();
    });
  });

  // βœ… Testing complex async workflows
  describe('processUserRegistration', () => {
    it('should handle complex registration workflow', async () => {
      // Arrange
      const userData = {
        name: 'Jane Doe',
        email: '[email protected]',
        isActive: true
      };

      const mockTransaction = {
        query: jest.fn().mockResolvedValue([]),
        commit: jest.fn(),
        rollback: jest.fn()
      };

      mockDb.transaction.mockImplementation(async (callback) => {
        return await callback(mockTransaction);
      });

      mockCache.set.mockResolvedValue();
      mockEmailService.sendEmail.mockResolvedValue();

      // Act
      const result = await userService.processUserRegistration(userData);

      // Assert
      expect(result.user).toMatchObject({
        name: userData.name,
        email: userData.email,
        isActive: userData.isActive
      });
      expect(result.emailSent).toBe(true);
      expect(result.cacheUpdated).toBe(true);

      // Verify transaction operations
      expect(mockTransaction.query).toHaveBeenCalledTimes(2);
      expect(mockCache.set).toHaveBeenCalled();
      expect(mockEmailService.sendEmail).toHaveBeenCalled();
    });

    it('should handle partial failures gracefully', async () => {
      // Arrange
      const userData = {
        name: 'Jane Doe',
        email: '[email protected]',
        isActive: true
      };

      const mockTransaction = {
        query: jest.fn().mockResolvedValue([]),
        commit: jest.fn(),
        rollback: jest.fn()
      };

      mockDb.transaction.mockImplementation(async (callback) => {
        return await callback(mockTransaction);
      });

      // Cache succeeds, email fails
      mockCache.set.mockResolvedValue();
      mockEmailService.sendEmail.mockRejectedValue(new Error('Email service down'));

      // Act
      const result = await userService.processUserRegistration(userData);

      // Assert
      expect(result.user).toBeDefined();
      expect(result.emailSent).toBe(false); // Email failed
      expect(result.cacheUpdated).toBe(true); // Cache succeeded
    });
  });
});

// Advanced promise testing patterns
describe('Advanced Promise Testing', () => {
  // βœ… Testing promise timing and order
  it('should handle promise execution order correctly', async () => {
    const executionOrder: string[] = [];
    
    const promise1 = new Promise<void>(resolve => {
      setTimeout(() => {
        executionOrder.push('promise1');
        resolve();
      }, 100);
    });

    const promise2 = new Promise<void>(resolve => {
      setTimeout(() => {
        executionOrder.push('promise2');
        resolve();
      }, 50);
    });

    const promise3 = new Promise<void>(resolve => {
      setTimeout(() => {
        executionOrder.push('promise3');
        resolve();
      }, 150);
    });

    // Wait for all promises
    await Promise.all([promise1, promise2, promise3]);

    // Verify execution order (shortest timeout first)
    expect(executionOrder).toEqual(['promise2', 'promise1', 'promise3']);
  });

  // βœ… Testing Promise.allSettled behavior
  it('should handle mixed success and failure with allSettled', async () => {
    const promises = [
      Promise.resolve('success1'),
      Promise.reject(new Error('error1')),
      Promise.resolve('success2'),
      Promise.reject(new Error('error2'))
    ];

    const results = await Promise.allSettled(promises);

    expect(results).toHaveLength(4);
    expect(results[0].status).toBe('fulfilled');
    expect(results[1].status).toBe('rejected');
    expect(results[2].status).toBe('fulfilled');
    expect(results[3].status).toBe('rejected');

    // Type-safe access to results
    if (results[0].status === 'fulfilled') {
      expect(results[0].value).toBe('success1');
    }

    if (results[1].status === 'rejected') {
      expect(results[1].reason).toBeInstanceOf(Error);
      expect(results[1].reason.message).toBe('error1');
    }
  });

  // βœ… Testing promise race conditions
  it('should handle promise race correctly', async () => {
    const fastPromise = new Promise<string>(resolve => {
      setTimeout(() => resolve('fast'), 50);
    });

    const slowPromise = new Promise<string>(resolve => {
      setTimeout(() => resolve('slow'), 200);
    });

    const result = await Promise.race([slowPromise, fastPromise]);
    expect(result).toBe('fast');
  });
});

πŸ”„ Testing Callback-Based Async Code

πŸ“ž Handling Legacy Callback Patterns

Many legacy APIs use callbacks instead of promises. Here’s how to test them effectively.

// 🌟 Comprehensive callback testing patterns

describe('UserService - Callback Testing', () => {
  let userService: UserService;
  let mockDb: jest.Mocked<DatabaseConnection>;
  let mockEmailService: jest.Mocked<EmailService>;
  let mockCache: jest.Mocked<CacheService>;

  beforeEach(() => {
    mockDb = {
      connect: jest.fn(),
      disconnect: jest.fn(),
      query: jest.fn(),
      transaction: jest.fn()
    };

    mockEmailService = {
      sendEmail: jest.fn(),
      validateEmail: jest.fn()
    };

    mockCache = {
      get: jest.fn(),
      set: jest.fn(),
      delete: jest.fn(),
      clear: jest.fn()
    };

    userService = new UserService(mockDb, mockEmailService, mockCache);
  });

  // βœ… Test successful callback execution
  describe('getUserById (callback)', () => {
    it('should call callback with user when found in cache', (done) => {
      // Arrange
      const userId = 'test-user-id';
      const cachedUser: User = {
        id: userId,
        name: 'John Doe',
        email: '[email protected]',
        isActive: true,
        createdAt: new Date()
      };

      mockCache.get.mockResolvedValue(cachedUser);

      // Act
      userService.getUserById(userId, (error, user) => {
        try {
          // Assert
          expect(error).toBeNull();
          expect(user).toEqual(cachedUser);
          expect(mockCache.get).toHaveBeenCalledWith(`user:${userId}`);
          
          // Don't expect database query since cache hit
          expect(mockDb.query).not.toHaveBeenCalled();
          
          done();
        } catch (err) {
          done(err);
        }
      });
    });

    it('should call callback with user when found in database', (done) => {
      // Arrange
      const userId = 'test-user-id';
      const dbUser: User = {
        id: userId,
        name: 'Jane Doe',
        email: '[email protected]',
        isActive: true,
        createdAt: new Date()
      };

      mockCache.get.mockResolvedValue(null); // Cache miss
      mockDb.query.mockResolvedValue([dbUser]);
      mockCache.set.mockResolvedValue();

      // Act
      userService.getUserById(userId, (error, user) => {
        try {
          // Assert
          expect(error).toBeNull();
          expect(user).toEqual(dbUser);
          expect(mockCache.get).toHaveBeenCalledWith(`user:${userId}`);
          expect(mockDb.query).toHaveBeenCalledWith(
            'SELECT * FROM users WHERE id = ?',
            [userId]
          );
          expect(mockCache.set).toHaveBeenCalledWith(`user:${userId}`, dbUser, 3600);
          
          done();
        } catch (err) {
          done(err);
        }
      });
    });

    it('should call callback with error when user not found', (done) => {
      // Arrange
      const userId = 'nonexistent-user';

      mockCache.get.mockResolvedValue(null);
      mockDb.query.mockResolvedValue([]); // No users found

      // Act
      userService.getUserById(userId, (error, user) => {
        try {
          // Assert
          expect(error).toBeInstanceOf(Error);
          expect(error?.message).toBe('User not found');
          expect(user).toBeUndefined();
          
          done();
        } catch (err) {
          done(err);
        }
      });
    });

    it('should call callback with error when database fails', (done) => {
      // Arrange
      const userId = 'test-user-id';
      const dbError = new Error('Database connection failed');

      mockCache.get.mockResolvedValue(null);
      mockDb.query.mockRejectedValue(dbError);

      // Act
      userService.getUserById(userId, (error, user) => {
        try {
          // Assert
          expect(error).toBe(dbError);
          expect(user).toBeUndefined();
          
          done();
        } catch (err) {
          done(err);
        }
      });
    });
  });

  // βœ… Converting callback tests to promise-based tests
  describe('getUserById (promisified)', () => {
    // Helper function to promisify callback-based method
    const getUserByIdPromise = (id: string): Promise<User> => {
      return new Promise((resolve, reject) => {
        userService.getUserById(id, (error, user) => {
          if (error) {
            reject(error);
          } else if (user) {
            resolve(user);
          } else {
            reject(new Error('Unknown error'));
          }
        });
      });
    };

    it('should resolve with user when found', async () => {
      // Arrange
      const userId = 'test-user-id';
      const user: User = {
        id: userId,
        name: 'John Doe',
        email: '[email protected]',
        isActive: true,
        createdAt: new Date()
      };

      mockCache.get.mockResolvedValue(user);

      // Act & Assert
      await expect(getUserByIdPromise(userId)).resolves.toEqual(user);
    });

    it('should reject when user not found', async () => {
      // Arrange
      const userId = 'nonexistent-user';

      mockCache.get.mockResolvedValue(null);
      mockDb.query.mockResolvedValue([]);

      // Act & Assert
      await expect(getUserByIdPromise(userId))
        .rejects
        .toThrow('User not found');
    });
  });

  // βœ… Testing callback timeouts
  describe('getUserWithTimeout', () => {
    it('should resolve within timeout', async () => {
      // Arrange
      const userId = 'test-user-id';
      const user: User = {
        id: userId,
        name: 'John Doe',
        email: '[email protected]',
        isActive: true,
        createdAt: new Date()
      };

      mockCache.get.mockResolvedValue(user);

      // Act & Assert
      await expect(userService.getUserWithTimeout(userId, 1000))
        .resolves
        .toEqual(user);
    });

    it('should reject when operation times out', async () => {
      // Arrange
      const userId = 'test-user-id';
      
      // Make cache operation take longer than timeout
      mockCache.get.mockImplementation(() => 
        new Promise(resolve => setTimeout(() => resolve(null), 200))
      );
      mockDb.query.mockImplementation(() =>
        new Promise(resolve => setTimeout(() => resolve([]), 200))
      );

      // Act & Assert
      await expect(userService.getUserWithTimeout(userId, 100))
        .rejects
        .toThrow('Operation timed out');
    });
  });
});

// Advanced callback testing patterns
describe('Advanced Callback Testing', () => {
  // βœ… Testing multiple callback invocations
  it('should handle multiple callback calls correctly', (done) => {
    let callCount = 0;
    const expectedCalls = 3;

    const callback = (result: string) => {
      callCount++;
      expect(result).toBe(`result-${callCount}`);
      
      if (callCount === expectedCalls) {
        done();
      }
    };

    // Simulate multiple async operations
    for (let i = 1; i <= expectedCalls; i++) {
      setTimeout(() => {
        callback(`result-${i}`);
      }, i * 10);
    }
  });

  // βœ… Testing callback error handling
  it('should handle callback errors gracefully', (done) => {
    const errors: Error[] = [];
    
    const errorCallback = (error: Error | null, result?: string) => {
      if (error) {
        errors.push(error);
      }
      
      if (errors.length === 2) {
        expect(errors).toHaveLength(2);
        expect(errors[0].message).toBe('First error');
        expect(errors[1].message).toBe('Second error');
        done();
      }
    };

    // Simulate multiple error scenarios
    setTimeout(() => {
      errorCallback(new Error('First error'));
    }, 10);

    setTimeout(() => {
      errorCallback(new Error('Second error'));
    }, 20);
  });

  // βœ… Testing callback with custom matchers
  it('should use custom matchers for callback validation', (done) => {
    const customCallback = (data: { timestamp: Date; value: number }) => {
      try {
        expect(data).toEqual({
          timestamp: expect.any(Date),
          value: expect.any(Number)
        });
        
        expect(data.timestamp.getTime()).toBeCloseTo(Date.now(), -2);
        expect(data.value).toBeGreaterThan(0);
        
        done();
      } catch (error) {
        done(error);
      }
    };

    setTimeout(() => {
      customCallback({
        timestamp: new Date(),
        value: Math.random() * 100
      });
    }, 50);
  });
});

⏰ Testing Timing and Race Conditions

πŸƒβ€β™‚οΈ Handling Time-Sensitive Operations

Time-sensitive operations require special testing techniques to ensure reliability and avoid flaky tests.

// 🌟 Comprehensive timing and race condition testing

describe('Timing and Race Condition Testing', () => {
  // βœ… Testing with fake timers
  describe('Timer-based operations', () => {
    beforeEach(() => {
      jest.useFakeTimers();
    });

    afterEach(() => {
      jest.useRealTimers();
    });

    it('should execute delayed operation after specified time', () => {
      // Arrange
      const callback = jest.fn();
      const delay = 1000;

      // Act
      setTimeout(callback, delay);

      // Assert - callback should not be called yet
      expect(callback).not.toHaveBeenCalled();

      // Fast-forward time
      jest.advanceTimersByTime(delay);

      // Assert - callback should now be called
      expect(callback).toHaveBeenCalledTimes(1);
    });

    it('should handle multiple timers correctly', () => {
      // Arrange
      const callbacks = {
        fast: jest.fn(),
        medium: jest.fn(),
        slow: jest.fn()
      };

      // Act - set up multiple timers
      setTimeout(callbacks.fast, 100);
      setTimeout(callbacks.medium, 500);
      setTimeout(callbacks.slow, 1000);

      // Assert - no callbacks called yet
      expect(callbacks.fast).not.toHaveBeenCalled();
      expect(callbacks.medium).not.toHaveBeenCalled();
      expect(callbacks.slow).not.toHaveBeenCalled();

      // Fast-forward to 100ms
      jest.advanceTimersByTime(100);
      expect(callbacks.fast).toHaveBeenCalledTimes(1);
      expect(callbacks.medium).not.toHaveBeenCalled();
      expect(callbacks.slow).not.toHaveBeenCalled();

      // Fast-forward to 500ms total
      jest.advanceTimersByTime(400);
      expect(callbacks.fast).toHaveBeenCalledTimes(1);
      expect(callbacks.medium).toHaveBeenCalledTimes(1);
      expect(callbacks.slow).not.toHaveBeenCalled();

      // Fast-forward to 1000ms total
      jest.advanceTimersByTime(500);
      expect(callbacks.fast).toHaveBeenCalledTimes(1);
      expect(callbacks.medium).toHaveBeenCalledTimes(1);
      expect(callbacks.slow).toHaveBeenCalledTimes(1);
    });

    it('should handle interval operations', () => {
      // Arrange
      const callback = jest.fn();
      const interval = 250;

      // Act
      const intervalId = setInterval(callback, interval);

      // Assert - advance time and check calls
      jest.advanceTimersByTime(interval);
      expect(callback).toHaveBeenCalledTimes(1);

      jest.advanceTimersByTime(interval);
      expect(callback).toHaveBeenCalledTimes(2);

      jest.advanceTimersByTime(interval);
      expect(callback).toHaveBeenCalledTimes(3);

      // Clean up
      clearInterval(intervalId);
    });
  });

  // βœ… Testing debounce and throttle functions
  describe('Debounce and Throttle', () => {
    // Simple debounce implementation for testing
    function debounce<T extends (...args: any[]) => any>(
      func: T,
      wait: number
    ): T {
      let timeout: NodeJS.Timeout;
      
      return ((...args: Parameters<T>) => {
        clearTimeout(timeout);
        timeout = setTimeout(() => func(...args), wait);
      }) as T;
    }

    // Simple throttle implementation for testing
    function throttle<T extends (...args: any[]) => any>(
      func: T,
      wait: number
    ): T {
      let lastTime = 0;
      
      return ((...args: Parameters<T>) => {
        const now = Date.now();
        if (now - lastTime >= wait) {
          lastTime = now;
          return func(...args);
        }
      }) as T;
    }

    beforeEach(() => {
      jest.useFakeTimers();
    });

    afterEach(() => {
      jest.useRealTimers();
    });

    it('should debounce function calls correctly', () => {
      // Arrange
      const originalFunc = jest.fn();
      const debouncedFunc = debounce(originalFunc, 100);

      // Act - call multiple times rapidly
      debouncedFunc('call1');
      debouncedFunc('call2');
      debouncedFunc('call3');

      // Assert - no calls yet
      expect(originalFunc).not.toHaveBeenCalled();

      // Fast-forward time
      jest.advanceTimersByTime(100);

      // Assert - only last call should be executed
      expect(originalFunc).toHaveBeenCalledTimes(1);
      expect(originalFunc).toHaveBeenCalledWith('call3');
    });

    it('should throttle function calls correctly', () => {
      // Arrange
      const originalFunc = jest.fn();
      const throttledFunc = throttle(originalFunc, 100);

      // Mock Date.now
      let currentTime = 0;
      jest.spyOn(Date, 'now').mockImplementation(() => currentTime);

      // Act & Assert
      throttledFunc('call1');
      expect(originalFunc).toHaveBeenCalledTimes(1);
      expect(originalFunc).toHaveBeenCalledWith('call1');

      // Call again immediately - should be ignored
      throttledFunc('call2');
      expect(originalFunc).toHaveBeenCalledTimes(1);

      // Advance time and call again
      currentTime += 100;
      throttledFunc('call3');
      expect(originalFunc).toHaveBeenCalledTimes(2);
      expect(originalFunc).toHaveBeenCalledWith('call3');
    });
  });

  // βœ… Testing race conditions
  describe('Race Conditions', () => {
    it('should handle concurrent async operations correctly', async () => {
      // Arrange
      const results: string[] = [];
      const delays = [100, 50, 150, 75];

      const createAsyncOperation = (id: string, delay: number) => {
        return new Promise<void>(resolve => {
          setTimeout(() => {
            results.push(id);
            resolve();
          }, delay);
        });
      };

      // Act - start all operations concurrently
      const operations = delays.map((delay, index) => 
        createAsyncOperation(`op${index + 1}`, delay)
      );

      await Promise.all(operations);

      // Assert - results should be in order of completion (by delay)
      expect(results).toEqual(['op2', 'op4', 'op1', 'op3']);
    });

    it('should handle resource contention correctly', async () => {
      // Arrange
      let sharedResource = 0;
      const operations: Promise<number>[] = [];

      const incrementResource = async (increment: number, delay: number): Promise<number> => {
        // Simulate async operation
        await new Promise(resolve => setTimeout(resolve, delay));
        
        // Critical section - potential race condition
        const currentValue = sharedResource;
        await new Promise(resolve => setTimeout(resolve, 10)); // Simulate processing time
        sharedResource = currentValue + increment;
        
        return sharedResource;
      };

      // Act - start multiple operations that modify shared resource
      for (let i = 1; i <= 5; i++) {
        operations.push(incrementResource(i, Math.random() * 50));
      }

      const results = await Promise.all(operations);

      // Assert - verify final state
      expect(sharedResource).toBeGreaterThan(0);
      expect(results).toHaveLength(5);
      
      // Each result should reflect the state at the time of completion
      results.forEach(result => {
        expect(result).toBeGreaterThan(0);
      });
    });

    it('should handle async resource cleanup correctly', async () => {
      // Arrange
      const resources: string[] = [];
      const cleanupOrder: string[] = [];

      const createResource = async (id: string): Promise<() => Promise<void>> => {
        resources.push(id);
        
        // Return cleanup function
        return async () => {
          await new Promise(resolve => setTimeout(resolve, Math.random() * 50));
          cleanupOrder.push(id);
          const index = resources.indexOf(id);
          if (index > -1) {
            resources.splice(index, 1);
          }
        };
      };

      // Act - create resources and cleanup functions
      const cleanup1 = await createResource('resource1');
      const cleanup2 = await createResource('resource2');
      const cleanup3 = await createResource('resource3');

      expect(resources).toEqual(['resource1', 'resource2', 'resource3']);

      // Clean up concurrently
      await Promise.all([cleanup1(), cleanup2(), cleanup3()]);

      // Assert - all resources cleaned up
      expect(resources).toEqual([]);
      expect(cleanupOrder).toHaveLength(3);
      expect(cleanupOrder).toEqual(expect.arrayContaining(['resource1', 'resource2', 'resource3']));
    });
  });
});

// Real-world async testing scenarios
describe('Real-World Async Testing', () => {
  let apiClient: ApiClient;

  beforeEach(() => {
    apiClient = new ApiClient('https://api.example.com');
    
    // Mock fetch globally
    global.fetch = jest.fn();
  });

  afterEach(() => {
    jest.resetAllMocks();
  });

  // βœ… Testing API retry logic
  describe('API Retry Logic', () => {
    it('should retry failed requests and eventually succeed', async () => {
      // Arrange
      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      
      // First two calls fail, third succeeds
      mockFetch
        .mockRejectedValueOnce(new Error('Network error'))
        .mockRejectedValueOnce(new Error('Service unavailable'))
        .mockResolvedValueOnce({
          ok: true,
          status: 200,
          statusText: 'OK',
          json: () => Promise.resolve({ id: 1, name: 'Test User' })
        } as Response);

      // Act
      const result = await apiClient.getWithRetry<{ id: number; name: string }>('/users/1');

      // Assert
      expect(result.status).toBe(200);
      expect(result.data).toEqual({ id: 1, name: 'Test User' });
      expect(mockFetch).toHaveBeenCalledTimes(3);
    });

    it('should fail after maximum retries', async () => {
      // Arrange
      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      
      // All calls fail
      mockFetch.mockRejectedValue(new Error('Persistent error'));

      // Act & Assert
      await expect(apiClient.getWithRetry('/users/1', 2))
        .rejects
        .toThrow('Persistent error');

      expect(mockFetch).toHaveBeenCalledTimes(3); // Initial + 2 retries
    });
  });

  // βœ… Testing concurrent operations
  describe('Concurrent Operations', () => {
    it('should handle multiple concurrent API calls', async () => {
      // Arrange
      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      
      const users = [
        { id: 1, name: 'User 1' },
        { id: 2, name: 'User 2' },
        { id: 3, name: 'User 3' }
      ];

      mockFetch.mockImplementation((url) => {
        const userId = url.toString().split('/').pop();
        const user = users.find(u => u.id.toString() === userId);
        
        return Promise.resolve({
          ok: true,
          status: 200,
          statusText: 'OK',
          json: () => Promise.resolve(user)
        } as Response);
      });

      // Act - make concurrent requests
      const requests = users.map(user => 
        apiClient.get<typeof user>(`/users/${user.id}`)
      );

      const results = await Promise.all(requests);

      // Assert
      expect(results).toHaveLength(3);
      results.forEach((result, index) => {
        expect(result.status).toBe(200);
        expect(result.data).toEqual(users[index]);
      });

      expect(mockFetch).toHaveBeenCalledTimes(3);
    });

    it('should handle mixed success and failure in concurrent operations', async () => {
      // Arrange
      const mockFetch = global.fetch as jest.MockedFunction<typeof fetch>;
      
      mockFetch
        .mockResolvedValueOnce({
          ok: true,
          status: 200,
          statusText: 'OK',
          json: () => Promise.resolve({ id: 1, name: 'User 1' })
        } as Response)
        .mockRejectedValueOnce(new Error('Not found'))
        .mockResolvedValueOnce({
          ok: true,
          status: 200,
          statusText: 'OK',
          json: () => Promise.resolve({ id: 3, name: 'User 3' })
        } as Response);

      // Act
      const requests = [
        apiClient.get('/users/1'),
        apiClient.get('/users/2'),
        apiClient.get('/users/3')
      ];

      const results = await Promise.allSettled(requests);

      // Assert
      expect(results).toHaveLength(3);
      expect(results[0].status).toBe('fulfilled');
      expect(results[1].status).toBe('rejected');
      expect(results[2].status).toBe('fulfilled');

      if (results[0].status === 'fulfilled') {
        expect(results[0].value.data).toEqual({ id: 1, name: 'User 1' });
      }

      if (results[1].status === 'rejected') {
        expect(results[1].reason).toBeInstanceOf(Error);
        expect(results[1].reason.message).toBe('Not found');
      }

      if (results[2].status === 'fulfilled') {
        expect(results[2].value.data).toEqual({ id: 3, name: 'User 3' });
      }
    });
  });
});

🎭 Mocking Async Dependencies

πŸ”§ Advanced Async Mocking Patterns

Mocking async dependencies requires careful handling of timing, promises, and side effects.

// 🌟 Comprehensive async mocking patterns

describe('Advanced Async Mocking', () => {
  // βœ… Mocking async functions with different behaviors
  describe('Async Function Mocking', () => {
    interface WeatherService {
      getCurrentWeather(city: string): Promise<{
        temperature: number;
        humidity: number;
        description: string;
      }>;
      getWeatherForecast(city: string, days: number): Promise<Array<{
        date: string;
        temperature: number;
        description: string;
      }>>;
    }

    let mockWeatherService: jest.Mocked<WeatherService>;

    beforeEach(() => {
      mockWeatherService = {
        getCurrentWeather: jest.fn(),
        getWeatherForecast: jest.fn()
      };
    });

    it('should mock successful async operations', async () => {
      // Arrange
      const expectedWeather = {
        temperature: 25,
        humidity: 60,
        description: 'Sunny'
      };

      mockWeatherService.getCurrentWeather.mockResolvedValue(expectedWeather);

      // Act
      const result = await mockWeatherService.getCurrentWeather('London');

      // Assert
      expect(result).toEqual(expectedWeather);
      expect(mockWeatherService.getCurrentWeather).toHaveBeenCalledWith('London');
    });

    it('should mock async operations with delays', async () => {
      // Arrange
      const expectedWeather = {
        temperature: 20,
        humidity: 70,
        description: 'Cloudy'
      };

      mockWeatherService.getCurrentWeather.mockImplementation((city) => {
        return new Promise(resolve => {
          setTimeout(() => {
            resolve(expectedWeather);
          }, 100);
        });
      });

      // Act
      const startTime = Date.now();
      const result = await mockWeatherService.getCurrentWeather('Paris');
      const endTime = Date.now();

      // Assert
      expect(result).toEqual(expectedWeather);
      expect(endTime - startTime).toBeGreaterThanOrEqual(100);
    });

    it('should mock async operations with conditional behavior', async () => {
      // Arrange
      mockWeatherService.getCurrentWeather.mockImplementation(async (city) => {
        if (city === 'InvalidCity') {
          throw new Error('City not found');
        }
        
        if (city === 'London') {
          return {
            temperature: 18,
            humidity: 65,
            description: 'Rainy'
          };
        }
        
        return {
          temperature: 25,
          humidity: 50,
          description: 'Sunny'
        };
      });

      // Act & Assert
      await expect(mockWeatherService.getCurrentWeather('InvalidCity'))
        .rejects
        .toThrow('City not found');

      const londonWeather = await mockWeatherService.getCurrentWeather('London');
      expect(londonWeather.description).toBe('Rainy');

      const defaultWeather = await mockWeatherService.getCurrentWeather('Tokyo');
      expect(defaultWeather.description).toBe('Sunny');
    });

    it('should mock async operations with sequence of responses', async () => {
      // Arrange - different responses for multiple calls
      mockWeatherService.getCurrentWeather
        .mockResolvedValueOnce({
          temperature: 20,
          humidity: 60,
          description: 'Morning clouds'
        })
        .mockResolvedValueOnce({
          temperature: 25,
          humidity: 55,
          description: 'Afternoon sun'
        })
        .mockResolvedValueOnce({
          temperature: 18,
          humidity: 70,
          description: 'Evening rain'
        });

      // Act
      const morning = await mockWeatherService.getCurrentWeather('London');
      const afternoon = await mockWeatherService.getCurrentWeather('London');
      const evening = await mockWeatherService.getCurrentWeather('London');

      // Assert
      expect(morning.description).toBe('Morning clouds');
      expect(afternoon.description).toBe('Afternoon sun');
      expect(evening.description).toBe('Evening rain');
    });
  });

  // βœ… Mocking external API calls
  describe('External API Mocking', () => {
    class ExternalApiService {
      async fetchUserData(userId: string): Promise<{
        id: string;
        name: string;
        email: string;
      }> {
        const response = await fetch(`https://api.example.com/users/${userId}`);
        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        return response.json();
      }

      async createUser(userData: {
        name: string;
        email: string;
      }): Promise<{
        id: string;
        name: string;
        email: string;
        createdAt: string;
      }> {
        const response = await fetch('https://api.example.com/users', {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(userData)
        });

        if (!response.ok) {
          throw new Error(`HTTP ${response.status}: ${response.statusText}`);
        }
        
        return response.json();
      }
    }

    let apiService: ExternalApiService;
    let mockFetch: jest.MockedFunction<typeof fetch>;

    beforeEach(() => {
      apiService = new ExternalApiService();
      mockFetch = jest.fn();
      global.fetch = mockFetch;
    });

    afterEach(() => {
      jest.resetAllMocks();
    });

    it('should mock successful API response', async () => {
      // Arrange
      const expectedUser = {
        id: 'user-123',
        name: 'John Doe',
        email: '[email protected]'
      };

      mockFetch.mockResolvedValue({
        ok: true,
        status: 200,
        statusText: 'OK',
        json: () => Promise.resolve(expectedUser)
      } as Response);

      // Act
      const result = await apiService.fetchUserData('user-123');

      // Assert
      expect(result).toEqual(expectedUser);
      expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/users/user-123');
    });

    it('should mock API error responses', async () => {
      // Arrange
      mockFetch.mockResolvedValue({
        ok: false,
        status: 404,
        statusText: 'Not Found'
      } as Response);

      // Act & Assert
      await expect(apiService.fetchUserData('nonexistent'))
        .rejects
        .toThrow('HTTP 404: Not Found');

      expect(mockFetch).toHaveBeenCalledWith('https://api.example.com/users/nonexistent');
    });

    it('should mock POST requests with request body validation', async () => {
      // Arrange
      const userData = {
        name: 'Jane Doe',
        email: '[email protected]'
      };

      const expectedResponse = {
        id: 'user-456',
        ...userData,
        createdAt: '2023-01-01T00:00:00Z'
      };

      mockFetch.mockResolvedValue({
        ok: true,
        status: 201,
        statusText: 'Created',
        json: () => Promise.resolve(expectedResponse)
      } as Response);

      // Act
      const result = await apiService.createUser(userData);

      // Assert
      expect(result).toEqual(expectedResponse);
      expect(mockFetch).toHaveBeenCalledWith(
        'https://api.example.com/users',
        {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(userData)
        }
      );
    });

    it('should mock network errors', async () => {
      // Arrange
      mockFetch.mockRejectedValue(new Error('Network error'));

      // Act & Assert
      await expect(apiService.fetchUserData('user-123'))
        .rejects
        .toThrow('Network error');
    });
  });

  // βœ… Mocking async operations with cleanup
  describe('Async Operations with Cleanup', () => {
    class ResourceManager {
      private resources: Map<string, any> = new Map();

      async acquireResource(id: string): Promise<{
        id: string;
        data: any;
        release: () => Promise<void>;
      }> {
        // Simulate async resource acquisition
        await new Promise(resolve => setTimeout(resolve, 50));

        const resource = {
          id,
          data: `resource-data-${id}`,
          acquired: new Date()
        };

        this.resources.set(id, resource);

        return {
          id,
          data: resource.data,
          release: async () => {
            await new Promise(resolve => setTimeout(resolve, 25));
            this.resources.delete(id);
          }
        };
      }

      getActiveResources(): string[] {
        return Array.from(this.resources.keys());
      }
    }

    let resourceManager: ResourceManager;
    let mockSetTimeout: jest.SpyInstance;
    let mockClearTimeout: jest.SpyInstance;

    beforeEach(() => {
      resourceManager = new ResourceManager();
      
      // Mock timers for controlled testing
      jest.useFakeTimers();
      mockSetTimeout = jest.spyOn(global, 'setTimeout');
      mockClearTimeout = jest.spyOn(global, 'clearTimeout');
    });

    afterEach(() => {
      jest.useRealTimers();
      jest.restoreAllMocks();
    });

    it('should handle resource acquisition and cleanup', async () => {
      // Act
      const resourcePromise = resourceManager.acquireResource('test-resource');
      
      // Fast-forward timers to complete acquisition
      jest.advanceTimersByTime(50);
      
      const resource = await resourcePromise;

      // Assert
      expect(resource.id).toBe('test-resource');
      expect(resource.data).toBe('resource-data-test-resource');
      expect(resourceManager.getActiveResources()).toContain('test-resource');

      // Act - release resource
      const releasePromise = resource.release();
      
      // Fast-forward timers to complete release
      jest.advanceTimersByTime(25);
      
      await releasePromise;

      // Assert
      expect(resourceManager.getActiveResources()).not.toContain('test-resource');
    });

    it('should handle multiple resources concurrently', async () => {
      // Act - acquire multiple resources
      const resource1Promise = resourceManager.acquireResource('resource-1');
      const resource2Promise = resourceManager.acquireResource('resource-2');
      const resource3Promise = resourceManager.acquireResource('resource-3');

      // Fast-forward timers
      jest.advanceTimersByTime(50);

      const [resource1, resource2, resource3] = await Promise.all([
        resource1Promise,
        resource2Promise,
        resource3Promise
      ]);

      // Assert
      expect(resourceManager.getActiveResources()).toEqual(
        expect.arrayContaining(['resource-1', 'resource-2', 'resource-3'])
      );

      // Act - release all resources concurrently
      const releasePromises = [
        resource1.release(),
        resource2.release(),
        resource3.release()
      ];

      jest.advanceTimersByTime(25);

      await Promise.all(releasePromises);

      // Assert
      expect(resourceManager.getActiveResources()).toEqual([]);
    });
  });
});

// Performance testing for async operations
describe('Async Performance Testing', () => {
  it('should measure async operation performance', async () => {
    // Arrange
    const operations = Array.from({ length: 100 }, (_, i) => i);
    
    const asyncOperation = async (value: number): Promise<number> => {
      // Simulate variable processing time
      await new Promise(resolve => setTimeout(resolve, Math.random() * 10));
      return value * 2;
    };

    // Act
    const startTime = Date.now();
    
    // Test sequential processing
    const sequentialResults: number[] = [];
    for (const op of operations) {
      sequentialResults.push(await asyncOperation(op));
    }
    
    const sequentialTime = Date.now() - startTime;

    // Test parallel processing
    const parallelStartTime = Date.now();
    const parallelResults = await Promise.all(
      operations.map(op => asyncOperation(op))
    );
    const parallelTime = Date.now() - parallelStartTime;

    // Assert
    expect(sequentialResults).toEqual(parallelResults);
    expect(parallelTime).toBeLessThan(sequentialTime);
    
    // Performance should be significantly better for parallel execution
    expect(parallelTime).toBeLessThan(sequentialTime * 0.5);
  }, 10000); // Extended timeout for performance test

  it('should handle async operation batching', async () => {
    // Arrange
    const batchSize = 5;
    const totalOperations = 20;
    const operations = Array.from({ length: totalOperations }, (_, i) => i);

    const batchProcessor = async (batch: number[]): Promise<number[]> => {
      // Simulate batch processing
      await new Promise(resolve => setTimeout(resolve, 50));
      return batch.map(n => n * 2);
    };

    // Act
    const batches: number[][] = [];
    for (let i = 0; i < operations.length; i += batchSize) {
      batches.push(operations.slice(i, i + batchSize));
    }

    const results = await Promise.all(
      batches.map(batch => batchProcessor(batch))
    );

    const flatResults = results.flat();

    // Assert
    expect(flatResults).toHaveLength(totalOperations);
    expect(flatResults).toEqual(operations.map(n => n * 2));
    expect(batches).toHaveLength(Math.ceil(totalOperations / batchSize));
  });
});

πŸ” Testing Error Handling in Async Code

🚨 Comprehensive Error Testing Patterns

Error handling in async code requires testing various failure scenarios and recovery mechanisms.

// 🌟 Comprehensive async error handling testing

describe('Async Error Handling', () => {
  // βœ… Testing promise rejection patterns
  describe('Promise Rejection Testing', () => {
    class AsyncValidator {
      async validateEmail(email: string): Promise<boolean> {
        if (!email) {
          throw new Error('Email is required');
        }
        
        if (!email.includes('@')) {
          throw new Error('Invalid email format');
        }
        
        // Simulate async validation
        await new Promise(resolve => setTimeout(resolve, 100));
        
        return true;
      }

      async validateUser(user: {
        email: string;
        age: number;
        name: string;
      }): Promise<void> {
        const errors: string[] = [];

        try {
          await this.validateEmail(user.email);
        } catch (error) {
          errors.push((error as Error).message);
        }

        if (user.age < 13) {
          errors.push('User must be at least 13 years old');
        }

        if (!user.name.trim()) {
          errors.push('Name is required');
        }

        if (errors.length > 0) {
          throw new Error(`Validation failed: ${errors.join(', ')}`);
        }
      }

      async validateWithTimeout<T>(
        validator: () => Promise<T>,
        timeoutMs: number
      ): Promise<T> {
        return Promise.race([
          validator(),
          new Promise<never>((_, reject) => {
            setTimeout(() => {
              reject(new Error('Validation timeout'));
            }, timeoutMs);
          })
        ]);
      }
    }

    let validator: AsyncValidator;

    beforeEach(() => {
      validator = new AsyncValidator();
    });

    it('should handle basic promise rejection', async () => {
      // Act & Assert
      await expect(validator.validateEmail(''))
        .rejects
        .toThrow('Email is required');

      await expect(validator.validateEmail('invalid-email'))
        .rejects
        .toThrow('Invalid email format');
    });

    it('should handle complex validation errors', async () => {
      // Arrange
      const invalidUser = {
        email: 'invalid',
        age: 10,
        name: ''
      };

      // Act & Assert
      await expect(validator.validateUser(invalidUser))
        .rejects
        .toThrow('Validation failed: Invalid email format, User must be at least 13 years old, Name is required');
    });

    it('should handle timeout errors', async () => {
      // Arrange
      const slowValidator = () => new Promise<boolean>(resolve => {
        setTimeout(() => resolve(true), 200);
      });

      // Act & Assert
      await expect(validator.validateWithTimeout(slowValidator, 100))
        .rejects
        .toThrow('Validation timeout');
    });

    it('should handle successful validation with timeout', async () => {
      // Arrange
      const fastValidator = () => new Promise<boolean>(resolve => {
        setTimeout(() => resolve(true), 50);
      });

      // Act & Assert
      await expect(validator.validateWithTimeout(fastValidator, 100))
        .resolves
        .toBe(true);
    });
  });

  // βœ… Testing error propagation in async chains
  describe('Error Propagation', () => {
    class DataProcessor {
      async fetchData(id: string): Promise<{ id: string; data: string }> {
        if (id === 'invalid') {
          throw new Error('Invalid ID');
        }
        
        return { id, data: `data-${id}` };
      }

      async processData(data: { id: string; data: string }): Promise<{
        id: string;
        processedData: string;
      }> {
        if (data.data.includes('error')) {
          throw new Error('Processing failed');
        }

        return {
          id: data.id,
          processedData: data.data.toUpperCase()
        };
      }

      async saveData(processedData: {
        id: string;
        processedData: string;
      }): Promise<void> {
        if (processedData.id === 'save-error') {
          throw new Error('Save failed');
        }
        
        // Simulate save operation
        await new Promise(resolve => setTimeout(resolve, 50));
      }

      async processWorkflow(id: string): Promise<void> {
        try {
          const data = await this.fetchData(id);
          const processedData = await this.processData(data);
          await this.saveData(processedData);
        } catch (error) {
          throw new Error(`Workflow failed: ${(error as Error).message}`);
        }
      }

      async processWorkflowWithRetry(
        id: string,
        maxRetries: number = 3
      ): Promise<void> {
        let lastError: Error;

        for (let attempt = 0; attempt <= maxRetries; attempt++) {
          try {
            await this.processWorkflow(id);
            return; // Success
          } catch (error) {
            lastError = error as Error;
            
            if (attempt === maxRetries) {
              throw new Error(`Max retries exceeded: ${lastError.message}`);
            }
            
            // Wait before retry
            await new Promise(resolve => setTimeout(resolve, 100 * (attempt + 1)));
          }
        }
      }
    }

    let processor: DataProcessor;

    beforeEach(() => {
      processor = new DataProcessor();
    });

    it('should propagate errors from fetch stage', async () => {
      // Act & Assert
      await expect(processor.processWorkflow('invalid'))
        .rejects
        .toThrow('Workflow failed: Invalid ID');
    });

    it('should propagate errors from processing stage', async () => {
      // Arrange
      jest.spyOn(processor, 'fetchData').mockResolvedValue({
        id: 'test',
        data: 'error-data'
      });

      // Act & Assert
      await expect(processor.processWorkflow('test'))
        .rejects
        .toThrow('Workflow failed: Processing failed');
    });

    it('should propagate errors from save stage', async () => {
      // Act & Assert
      await expect(processor.processWorkflow('save-error'))
        .rejects
        .toThrow('Workflow failed: Save failed');
    });

    it('should handle successful workflow', async () => {
      // Act & Assert
      await expect(processor.processWorkflow('valid-id'))
        .resolves
        .toBeUndefined();
    });

    it('should handle retry mechanism on failure', async () => {
      // Arrange
      let attemptCount = 0;
      jest.spyOn(processor, 'processWorkflow').mockImplementation(async () => {
        attemptCount++;
        if (attemptCount <= 2) {
          throw new Error('Temporary failure');
        }
        // Success on third attempt
      });

      // Act & Assert
      await expect(processor.processWorkflowWithRetry('test-id'))
        .resolves
        .toBeUndefined();

      expect(attemptCount).toBe(3);
    });

    it('should fail after maximum retries', async () => {
      // Arrange
      jest.spyOn(processor, 'processWorkflow').mockRejectedValue(
        new Error('Persistent failure')
      );

      // Act & Assert
      await expect(processor.processWorkflowWithRetry('test-id', 2))
        .rejects
        .toThrow('Max retries exceeded: Workflow failed: Persistent failure');
    });
  });

  // βœ… Testing async error boundaries
  describe('Async Error Boundaries', () => {
    class AsyncErrorBoundary {
      private errorHandlers: Map<string, (error: Error) => void> = new Map();
      private globalErrorHandler?: (error: Error) => void;

      setGlobalErrorHandler(handler: (error: Error) => void): void {
        this.globalErrorHandler = handler;
      }

      setErrorHandler(operationType: string, handler: (error: Error) => void): void {
        this.errorHandlers.set(operationType, handler);
      }

      async executeWithErrorBoundary<T>(
        operation: () => Promise<T>,
        operationType: string
      ): Promise<T | null> {
        try {
          return await operation();
        } catch (error) {
          const specificHandler = this.errorHandlers.get(operationType);
          
          if (specificHandler) {
            specificHandler(error as Error);
          } else if (this.globalErrorHandler) {
            this.globalErrorHandler(error as Error);
          } else {
            throw error; // Re-throw if no handler
          }
          
          return null;
        }
      }

      async executeBatch<T>(
        operations: Array<{
          operation: () => Promise<T>;
          type: string;
        }>
      ): Promise<Array<T | null>> {
        const results = await Promise.allSettled(
          operations.map(({ operation, type }) =>
            this.executeWithErrorBoundary(operation, type)
          )
        );

        return results.map(result => 
          result.status === 'fulfilled' ? result.value : null
        );
      }
    }

    let errorBoundary: AsyncErrorBoundary;
    let errorLogs: Array<{ type: string; message: string }>;

    beforeEach(() => {
      errorBoundary = new AsyncErrorBoundary();
      errorLogs = [];

      // Set up error handlers
      errorBoundary.setGlobalErrorHandler((error) => {
        errorLogs.push({ type: 'global', message: error.message });
      });

      errorBoundary.setErrorHandler('network', (error) => {
        errorLogs.push({ type: 'network', message: error.message });
      });

      errorBoundary.setErrorHandler('validation', (error) => {
        errorLogs.push({ type: 'validation', message: error.message });
      });
    });

    it('should handle errors with specific handlers', async () => {
      // Arrange
      const networkOperation = async () => {
        throw new Error('Network timeout');
      };

      const validationOperation = async () => {
        throw new Error('Invalid input');
      };

      // Act
      const networkResult = await errorBoundary.executeWithErrorBoundary(
        networkOperation,
        'network'
      );

      const validationResult = await errorBoundary.executeWithErrorBoundary(
        validationOperation,
        'validation'
      );

      // Assert
      expect(networkResult).toBeNull();
      expect(validationResult).toBeNull();
      expect(errorLogs).toEqual([
        { type: 'network', message: 'Network timeout' },
        { type: 'validation', message: 'Invalid input' }
      ]);
    });

    it('should handle errors with global handler', async () => {
      // Arrange
      const unknownOperation = async () => {
        throw new Error('Unknown error');
      };

      // Act
      const result = await errorBoundary.executeWithErrorBoundary(
        unknownOperation,
        'unknown'
      );

      // Assert
      expect(result).toBeNull();
      expect(errorLogs).toEqual([
        { type: 'global', message: 'Unknown error' }
      ]);
    });

    it('should handle batch operations with mixed results', async () => {
      // Arrange
      const operations = [
        {
          operation: async () => 'success1',
          type: 'success'
        },
        {
          operation: async () => {
            throw new Error('Network error');
          },
          type: 'network'
        },
        {
          operation: async () => 'success2',
          type: 'success'
        },
        {
          operation: async () => {
            throw new Error('Validation error');
          },
          type: 'validation'
        }
      ];

      // Act
      const results = await errorBoundary.executeBatch(operations);

      // Assert
      expect(results).toEqual(['success1', null, 'success2', null]);
      expect(errorLogs).toHaveLength(2);
      expect(errorLogs[0]).toEqual({ type: 'network', message: 'Network error' });
      expect(errorLogs[1]).toEqual({ type: 'validation', message: 'Validation error' });
    });
  });

  // βœ… Testing custom error types
  describe('Custom Error Types', () => {
    class NetworkError extends Error {
      constructor(message: string, public statusCode: number) {
        super(message);
        this.name = 'NetworkError';
      }
    }

    class ValidationError extends Error {
      constructor(message: string, public field: string) {
        super(message);
        this.name = 'ValidationError';
      }
    }

    class BusinessLogicError extends Error {
      constructor(message: string, public code: string) {
        super(message);
        this.name = 'BusinessLogicError';
      }
    }

    class AsyncService {
      async performOperation(data: {
        id: string;
        value: number;
      }): Promise<string> {
        // Validation
        if (!data.id) {
          throw new ValidationError('ID is required', 'id');
        }

        if (data.value < 0) {
          throw new ValidationError('Value must be positive', 'value');
        }

        // Business logic
        if (data.value > 1000) {
          throw new BusinessLogicError('Value exceeds maximum allowed', 'VALUE_TOO_HIGH');
        }

        // Network operation
        if (data.id === 'network-error') {
          throw new NetworkError('Service unavailable', 503);
        }

        return `Processed: ${data.id} with value ${data.value}`;
      }

      async handleErrors<T>(operation: () => Promise<T>): Promise<{
        result?: T;
        error?: {
          type: string;
          message: string;
          details?: any;
        };
      }> {
        try {
          const result = await operation();
          return { result };
        } catch (error) {
          if (error instanceof NetworkError) {
            return {
              error: {
                type: 'network',
                message: error.message,
                details: { statusCode: error.statusCode }
              }
            };
          }

          if (error instanceof ValidationError) {
            return {
              error: {
                type: 'validation',
                message: error.message,
                details: { field: error.field }
              }
            };
          }

          if (error instanceof BusinessLogicError) {
            return {
              error: {
                type: 'business',
                message: error.message,
                details: { code: error.code }
              }
            };
          }

          return {
            error: {
              type: 'unknown',
              message: (error as Error).message
            }
          };
        }
      }
    }

    let service: AsyncService;

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

    it('should throw and catch NetworkError', async () => {
      // Act & Assert
      await expect(service.performOperation({ id: 'network-error', value: 50 }))
        .rejects
        .toThrow(NetworkError);

      try {
        await service.performOperation({ id: 'network-error', value: 50 });
      } catch (error) {
        expect(error).toBeInstanceOf(NetworkError);
        expect((error as NetworkError).statusCode).toBe(503);
      }
    });

    it('should throw and catch ValidationError', async () => {
      // Act & Assert
      await expect(service.performOperation({ id: '', value: 50 }))
        .rejects
        .toThrow(ValidationError);

      try {
        await service.performOperation({ id: '', value: 50 });
      } catch (error) {
        expect(error).toBeInstanceOf(ValidationError);
        expect((error as ValidationError).field).toBe('id');
      }
    });

    it('should throw and catch BusinessLogicError', async () => {
      // Act & Assert
      await expect(service.performOperation({ id: 'test', value: 2000 }))
        .rejects
        .toThrow(BusinessLogicError);

      try {
        await service.performOperation({ id: 'test', value: 2000 });
      } catch (error) {
        expect(error).toBeInstanceOf(BusinessLogicError);
        expect((error as BusinessLogicError).code).toBe('VALUE_TOO_HIGH');
      }
    });

    it('should handle errors with typed error handling', async () => {
      // Test network error
      const networkResult = await service.handleErrors(() =>
        service.performOperation({ id: 'network-error', value: 50 })
      );

      expect(networkResult.error).toEqual({
        type: 'network',
        message: 'Service unavailable',
        details: { statusCode: 503 }
      });

      // Test validation error
      const validationResult = await service.handleErrors(() =>
        service.performOperation({ id: '', value: 50 })
      );

      expect(validationResult.error).toEqual({
        type: 'validation',
        message: 'ID is required',
        details: { field: 'id' }
      });

      // Test business logic error
      const businessResult = await service.handleErrors(() =>
        service.performOperation({ id: 'test', value: 2000 })
      );

      expect(businessResult.error).toEqual({
        type: 'business',
        message: 'Value exceeds maximum allowed',
        details: { code: 'VALUE_TOO_HIGH' }
      });

      // Test success case
      const successResult = await service.handleErrors(() =>
        service.performOperation({ id: 'test', value: 50 })
      );

      expect(successResult.result).toBe('Processed: test with value 50');
      expect(successResult.error).toBeUndefined();
    });
  });
});

🎯 Best Practices and Advanced Patterns

πŸ† Production-Ready Async Testing

Here are the most important patterns for reliable async testing in production applications.

// 🌟 Production-ready async testing patterns

describe('Production Async Testing Best Practices', () => {
  // βœ… Test isolation and cleanup
  describe('Test Isolation', () => {
    class ConnectionPool {
      private connections: Map<string, any> = new Map();
      private activeConnections = 0;

      async getConnection(id: string): Promise<any> {
        if (this.connections.has(id)) {
          return this.connections.get(id);
        }

        const connection = {
          id,
          created: new Date(),
          active: true
        };

        this.connections.set(id, connection);
        this.activeConnections++;
        
        return connection;
      }

      async releaseConnection(id: string): Promise<void> {
        if (this.connections.has(id)) {
          this.connections.delete(id);
          this.activeConnections--;
        }
      }

      async closeAllConnections(): Promise<void> {
        for (const id of this.connections.keys()) {
          await this.releaseConnection(id);
        }
      }

      getStats(): { total: number; active: number } {
        return {
          total: this.connections.size,
          active: this.activeConnections
        };
      }
    }

    let pool: ConnectionPool;

    beforeEach(() => {
      pool = new ConnectionPool();
    });

    afterEach(async () => {
      // Always clean up async resources
      await pool.closeAllConnections();
    });

    it('should properly isolate connection state between tests', async () => {
      // Act
      const conn1 = await pool.getConnection('test-1');
      const conn2 = await pool.getConnection('test-2');

      // Assert
      expect(pool.getStats().total).toBe(2);
      expect(pool.getStats().active).toBe(2);
    });

    it('should start with clean state', async () => {
      // This test should have no connections from previous test
      expect(pool.getStats().total).toBe(0);
      expect(pool.getStats().active).toBe(0);
    });
  });

  // βœ… Deterministic timing tests
  describe('Deterministic Timing', () => {
    class TaskScheduler {
      private tasks: Array<{
        id: string;
        delay: number;
        callback: () => void;
        scheduled: Date;
      }> = [];

      schedule(id: string, delay: number, callback: () => void): void {
        this.tasks.push({
          id,
          delay,
          callback,
          scheduled: new Date()
        });

        setTimeout(() => {
          callback();
          this.tasks = this.tasks.filter(t => t.id !== id);
        }, delay);
      }

      getTasks(): Array<{ id: string; delay: number; scheduled: Date }> {
        return this.tasks.map(({ id, delay, scheduled }) => ({
          id,
          delay,
          scheduled
        }));
      }

      clear(): void {
        this.tasks = [];
      }
    }

    let scheduler: TaskScheduler;

    beforeEach(() => {
      scheduler = new TaskScheduler();
      jest.useFakeTimers();
    });

    afterEach(() => {
      scheduler.clear();
      jest.useRealTimers();
    });

    it('should execute tasks in correct order with fake timers', () => {
      // Arrange
      const executionOrder: string[] = [];

      // Act
      scheduler.schedule('task-1', 100, () => executionOrder.push('task-1'));
      scheduler.schedule('task-2', 50, () => executionOrder.push('task-2'));
      scheduler.schedule('task-3', 150, () => executionOrder.push('task-3'));

      // Assert initial state
      expect(executionOrder).toEqual([]);
      expect(scheduler.getTasks()).toHaveLength(3);

      // Fast-forward time
      jest.advanceTimersByTime(50);
      expect(executionOrder).toEqual(['task-2']);

      jest.advanceTimersByTime(50);
      expect(executionOrder).toEqual(['task-2', 'task-1']);

      jest.advanceTimersByTime(50);
      expect(executionOrder).toEqual(['task-2', 'task-1', 'task-3']);
    });

    it('should handle concurrent task scheduling', () => {
      // Arrange
      const results: string[] = [];
      const taskCount = 10;

      // Act - schedule multiple tasks simultaneously
      for (let i = 0; i < taskCount; i++) {
        scheduler.schedule(
          `task-${i}`,
          Math.random() * 100, // Random delay up to 100ms
          () => results.push(`task-${i}`)
        );
      }

      // Fast-forward all timers
      jest.advanceTimersByTime(100);

      // Assert
      expect(results).toHaveLength(taskCount);
      expect(scheduler.getTasks()).toHaveLength(0); // All tasks completed
    });
  });

  // βœ… Comprehensive async integration testing
  describe('Async Integration Testing', () => {
    interface DatabaseService {
      connect(): Promise<void>;
      disconnect(): Promise<void>;
      query<T>(sql: string, params?: any[]): Promise<T[]>;
    }

    interface CacheService {
      get<T>(key: string): Promise<T | null>;
      set<T>(key: string, value: T, ttl?: number): Promise<void>;
    }

    interface NotificationService {
      sendNotification(userId: string, message: string): Promise<void>;
    }

    class UserManagementService {
      constructor(
        private db: DatabaseService,
        private cache: CacheService,
        private notifications: NotificationService
      ) {}

      async createUserWorkflow(userData: {
        name: string;
        email: string;
      }): Promise<{
        user: { id: string; name: string; email: string };
        cached: boolean;
        notificationSent: boolean;
      }> {
        // Create user in database
        const userId = `user-${Date.now()}`;
        await this.db.query(
          'INSERT INTO users (id, name, email) VALUES (?, ?, ?)',
          [userId, userData.name, userData.email]
        );

        const user = { id: userId, ...userData };

        // Parallel operations for better performance
        const [cacheResult, notificationResult] = await Promise.allSettled([
          this.cache.set(`user:${userId}`, user, 3600),
          this.notifications.sendNotification(userId, `Welcome ${userData.name}!`)
        ]);

        return {
          user,
          cached: cacheResult.status === 'fulfilled',
          notificationSent: notificationResult.status === 'fulfilled'
        };
      }

      async deleteUserWorkflow(userId: string): Promise<{
        deleted: boolean;
        cacheCleared: boolean;
        notificationSent: boolean;
      }> {
        let deleted = false;
        let cacheCleared = false;
        let notificationSent = false;

        try {
          // Delete from database
          await this.db.query('DELETE FROM users WHERE id = ?', [userId]);
          deleted = true;

          // Clear cache (non-critical)
          try {
            await this.cache.set(`user:${userId}`, null, 0);
            cacheCleared = true;
          } catch (error) {
            console.warn('Failed to clear cache:', error);
          }

          // Send notification (non-critical)
          try {
            await this.notifications.sendNotification(userId, 'Account deleted');
            notificationSent = true;
          } catch (error) {
            console.warn('Failed to send notification:', error);
          }
        } catch (error) {
          throw new Error(`Failed to delete user: ${(error as Error).message}`);
        }

        return { deleted, cacheCleared, notificationSent };
      }
    }

    let service: UserManagementService;
    let mockDb: jest.Mocked<DatabaseService>;
    let mockCache: jest.Mocked<CacheService>;
    let mockNotifications: jest.Mocked<NotificationService>;

    beforeEach(() => {
      mockDb = {
        connect: jest.fn(),
        disconnect: jest.fn(),
        query: jest.fn()
      };

      mockCache = {
        get: jest.fn(),
        set: jest.fn()
      };

      mockNotifications = {
        sendNotification: jest.fn()
      };

      service = new UserManagementService(mockDb, mockCache, mockNotifications);
    });

    it('should handle successful user creation workflow', async () => {
      // Arrange
      const userData = {
        name: 'John Doe',
        email: '[email protected]'
      };

      mockDb.query.mockResolvedValue([]);
      mockCache.set.mockResolvedValue();
      mockNotifications.sendNotification.mockResolvedValue();

      // Act
      const result = await service.createUserWorkflow(userData);

      // Assert
      expect(result.user.name).toBe(userData.name);
      expect(result.user.email).toBe(userData.email);
      expect(result.user.id).toMatch(/^user-\d+$/);
      expect(result.cached).toBe(true);
      expect(result.notificationSent).toBe(true);

      // Verify all services were called
      expect(mockDb.query).toHaveBeenCalledWith(
        'INSERT INTO users (id, name, email) VALUES (?, ?, ?)',
        [result.user.id, userData.name, userData.email]
      );
      expect(mockCache.set).toHaveBeenCalledWith(
        `user:${result.user.id}`,
        result.user,
        3600
      );
      expect(mockNotifications.sendNotification).toHaveBeenCalledWith(
        result.user.id,
        `Welcome ${userData.name}!`
      );
    });

    it('should handle partial failures in user creation', async () => {
      // Arrange
      const userData = {
        name: 'Jane Doe',
        email: '[email protected]'
      };

      mockDb.query.mockResolvedValue([]);
      mockCache.set.mockRejectedValue(new Error('Cache service down'));
      mockNotifications.sendNotification.mockResolvedValue();

      // Act
      const result = await service.createUserWorkflow(userData);

      // Assert
      expect(result.user).toBeDefined();
      expect(result.cached).toBe(false); // Cache failed
      expect(result.notificationSent).toBe(true); // Notification succeeded
    });

    it('should handle database failure in user creation', async () => {
      // Arrange
      const userData = {
        name: 'Bob Smith',
        email: '[email protected]'
      };

      mockDb.query.mockRejectedValue(new Error('Database connection failed'));

      // Act & Assert
      await expect(service.createUserWorkflow(userData))
        .rejects
        .toThrow('Database connection failed');

      // Verify no cache or notification calls were made
      expect(mockCache.set).not.toHaveBeenCalled();
      expect(mockNotifications.sendNotification).not.toHaveBeenCalled();
    });

    it('should handle user deletion workflow', async () => {
      // Arrange
      const userId = 'user-123';

      mockDb.query.mockResolvedValue([]);
      mockCache.set.mockResolvedValue();
      mockNotifications.sendNotification.mockResolvedValue();

      // Act
      const result = await service.deleteUserWorkflow(userId);

      // Assert
      expect(result.deleted).toBe(true);
      expect(result.cacheCleared).toBe(true);
      expect(result.notificationSent).toBe(true);

      expect(mockDb.query).toHaveBeenCalledWith(
        'DELETE FROM users WHERE id = ?',
        [userId]
      );
      expect(mockCache.set).toHaveBeenCalledWith(`user:${userId}`, null, 0);
      expect(mockNotifications.sendNotification).toHaveBeenCalledWith(
        userId,
        'Account deleted'
      );
    });

    it('should handle graceful degradation in deletion workflow', async () => {
      // Arrange
      const userId = 'user-123';

      mockDb.query.mockResolvedValue([]);
      mockCache.set.mockRejectedValue(new Error('Cache error'));
      mockNotifications.sendNotification.mockRejectedValue(new Error('Notification error'));

      // Act
      const result = await service.deleteUserWorkflow(userId);

      // Assert
      expect(result.deleted).toBe(true); // Main operation succeeded
      expect(result.cacheCleared).toBe(false); // Cache operation failed
      expect(result.notificationSent).toBe(false); // Notification failed
    });
  });

  // βœ… Performance and load testing
  describe('Async Performance Testing', () => {
    it('should handle high concurrency load', async () => {
      // Arrange
      const concurrentOperations = 100;
      const operations: Promise<number>[] = [];

      const asyncOperation = async (id: number): Promise<number> => {
        // Simulate variable processing time
        await new Promise(resolve => 
          setTimeout(resolve, Math.random() * 10)
        );
        return id * 2;
      };

      // Act
      const startTime = Date.now();
      
      for (let i = 0; i < concurrentOperations; i++) {
        operations.push(asyncOperation(i));
      }

      const results = await Promise.all(operations);
      const endTime = Date.now();

      // Assert
      expect(results).toHaveLength(concurrentOperations);
      expect(results[0]).toBe(0);
      expect(results[99]).toBe(198);
      
      // Should complete within reasonable time for concurrent execution
      expect(endTime - startTime).toBeLessThan(100);
    }, 10000);

    it('should handle memory pressure correctly', async () => {
      // Arrange
      const largeDataOperations = 50;
      const operations: Promise<string>[] = [];

      const createLargeData = async (id: number): Promise<string> => {
        // Create large string data
        const largeString = 'x'.repeat(10000);
        
        // Simulate async processing
        await new Promise(resolve => setTimeout(resolve, 1));
        
        return `${id}-${largeString}`;
      };

      // Act
      for (let i = 0; i < largeDataOperations; i++) {
        operations.push(createLargeData(i));
      }

      const results = await Promise.all(operations);

      // Assert
      expect(results).toHaveLength(largeDataOperations);
      results.forEach((result, index) => {
        expect(result).toMatch(new RegExp(`^${index}-x{10000}$`));
      });
    });
  });
});

πŸŽ‰ Conclusion

Congratulations! You’ve mastered the art of testing asynchronous TypeScript code! 🎯

πŸ”‘ Key Takeaways

  1. Promise Testing: Use async/await in tests for clean, readable async test code
  2. Callback Testing: Use done callback or promisify callback-based functions
  3. Timing Control: Use Jest’s fake timers for deterministic timing tests
  4. Error Handling: Test both success and failure scenarios comprehensively
  5. Mocking: Mock async dependencies with realistic behaviors and timing
  6. Race Conditions: Test concurrent operations and resource contention
  7. Performance: Test async performance and memory usage under load
  8. Integration: Test complete async workflows end-to-end

πŸš€ Next Steps

  • Testing Classes: Learn OOP testing patterns for TypeScript classes
  • Testing React Components: Master component testing with async operations
  • Testing Hooks: Test custom React hooks with async behavior
  • Integration Testing: Build comprehensive integration test suites
  • E2E Testing: Test complete user workflows with async operations

You now have the skills to test any asynchronous TypeScript code with confidence, ensuring your async operations work correctly under all conditions! 🌟