JavaScript Promises Tutorial - Section 7: Testing and Debugging
Async code is prone to:
Proper testing ensures reliability and catches edge cases.
Jest is the most popular JavaScript testing framework with built-in async support.
npm install --save-dev jest
// package.json
{
"scripts": {
"test": "jest"
}
}
// 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!
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');
});
test('fetches user successfully', async () => {
const user = await fetchUser(1);
expect(user.id).toBe(1);
expect(user.name).toBe('User 1');
});
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');
});
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 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'));
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);
});
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
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');
});
// 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);
});
});
});
async/await for cleaner test syntaxmockResolvedValue/mockRejectedValueexpect().rejects for rejection testingNext, we'll learn about debugging Promises and common pitfalls to avoid!