Learning Objectives

  • Learn how to test Promises with Jest
  • Master async/await in tests
  • Mock async functions effectively
  • Write comprehensive async test suites

Why Test Async Code?

Async code is prone to:

Proper testing ensures reliability and catches edge cases.

Testing with Jest

Jest is the most popular JavaScript testing framework with built-in async support.

Setup

npm install --save-dev jest
// package.json
{
  "scripts": {
    "test": "jest"
  }
}

Testing Promises with .then()

Basic Promise Test

// fetchUser.js
function fetchUser(id) {
  return new Promise((resolve, reject) => {
    setTimeout(() => {
      if (id > 0) {
        resolve({ id, name: `User ${id}` });
      } else {
        reject(new Error('Invalid ID'));
      }
    }, 100);
  });
}

// fetchUser.test.js
test('fetches user successfully', () => {
  return fetchUser(1).then(user => {
    expect(user.id).toBe(1);
    expect(user.name).toBe('User 1');
  });
});

Important: Return the Promise so Jest waits for it!

Testing Rejections

test('rejects with invalid ID', () => {
  return fetchUser(-1).catch(error => {
    expect(error.message).toBe('Invalid ID');
  });
});

// Or use expect().rejects
test('rejects with invalid ID', () => {
  return expect(fetchUser(-1)).rejects.toThrow('Invalid ID');
});

Testing with Async/Await

Basic Async Test

test('fetches user successfully', async () => {
  const user = await fetchUser(1);
  expect(user.id).toBe(1);
  expect(user.name).toBe('User 1');
});

Testing Rejections with Try/Catch

test('rejects with invalid ID', async () => {
  try {
    await fetchUser(-1);
    // Fail if no error thrown
    expect(true).toBe(false);
  } catch (error) {
    expect(error.message).toBe('Invalid ID');
  }
});

// Better: use expect().rejects
test('rejects with invalid ID', async () => {
  await expect(fetchUser(-1)).rejects.toThrow('Invalid ID');
});

Mocking Async Functions

Mock with jest.fn()

const mockFetch = jest.fn();

test('calls fetch with correct URL', async () => {
  mockFetch.mockResolvedValue({ data: 'test' });
  
  const result = await mockFetch('/api/users');
  
  expect(mockFetch).toHaveBeenCalledWith('/api/users');
  expect(result).toEqual({ data: 'test' });
});

Mock Resolved/Rejected Values

// Mock successful response
mockFetch.mockResolvedValue({ id: 1, name: 'John' });

// Mock error
mockFetch.mockRejectedValue(new Error('Network error'));

// Mock different responses for multiple calls
mockFetch
  .mockResolvedValueOnce({ id: 1 })
  .mockResolvedValueOnce({ id: 2 })
  .mockRejectedValueOnce(new Error('Failed'));

Testing Promise.all()

test('fetches multiple users in parallel', async () => {
  const users = await Promise.all([
    fetchUser(1),
    fetchUser(2),
    fetchUser(3)
  ]);
  
  expect(users).toHaveLength(3);
  expect(users[0].id).toBe(1);
  expect(users[1].id).toBe(2);
  expect(users[2].id).toBe(3);
});

Testing Timeouts

test('times out after 5 seconds', async () => {
  const slowPromise = new Promise(resolve => {
    setTimeout(() => resolve('done'), 10000);
  });
  
  await expect(
    timeout(slowPromise, 5000)
  ).rejects.toThrow('Timeout');
}, 6000); // Increase Jest timeout

Testing Retry Logic

test('retries failed requests', async () => {
  const mockFn = jest.fn()
    .mockRejectedValueOnce(new Error('Fail 1'))
    .mockRejectedValueOnce(new Error('Fail 2'))
    .mockResolvedValueOnce('Success');
  
  const result = await retry(mockFn, 3);
  
  expect(mockFn).toHaveBeenCalledTimes(3);
  expect(result).toBe('Success');
});

Complete Test Suite Example

// userService.test.js
describe('UserService', () => {
  let userService;
  let mockAPI;
  
  beforeEach(() => {
    mockAPI = {
      get: jest.fn(),
      post: jest.fn()
    };
    userService = new UserService(mockAPI);
  });
  
  describe('getUser', () => {
    test('fetches user successfully', async () => {
      mockAPI.get.mockResolvedValue({ id: 1, name: 'John' });
      
      const user = await userService.getUser(1);
      
      expect(mockAPI.get).toHaveBeenCalledWith('/users/1');
      expect(user).toEqual({ id: 1, name: 'John' });
    });
    
    test('handles errors', async () => {
      mockAPI.get.mockRejectedValue(new Error('Not found'));
      
      await expect(userService.getUser(999))
        .rejects.toThrow('Not found');
    });
  });
  
  describe('createUser', () => {
    test('creates user successfully', async () => {
      const newUser = { name: 'Jane' };
      mockAPI.post.mockResolvedValue({ id: 2, ...newUser });
      
      const user = await userService.createUser(newUser);
      
      expect(mockAPI.post).toHaveBeenCalledWith('/users', newUser);
      expect(user.id).toBe(2);
    });
  });
});

Best Practices

Key Takeaways

  • ✅ Always return or await Promises in Jest tests
  • ✅ Use async/await for cleaner test syntax
  • ✅ Mock async functions with mockResolvedValue/mockRejectedValue
  • ✅ Test both success and error cases
  • ✅ Use expect().rejects for rejection testing
  • ✅ Set appropriate test timeouts for slow operations

Next Steps

Next, we'll learn about debugging Promises and common pitfalls to avoid!