Testing Asynchronous Operations with Jest Promises
This challenge focuses on writing robust tests for asynchronous JavaScript code using Promises in Jest. You will learn how to effectively assert the outcomes of Promises, including successful resolutions and rejections, ensuring your asynchronous logic behaves as expected.
Problem Description
You are tasked with writing unit tests for a hypothetical userService that interacts with an external API to fetch user data. This service uses Promises to handle asynchronous operations. Your goal is to write Jest tests that verify the correct behavior of the userService in various scenarios, including successful data retrieval and API errors.
Key Requirements:
- Test Successful Resolution: Write a test to ensure that when the
fetchUserfunction successfully resolves with user data, the Promise is correctly handled and the expected data is returned. - Test Rejection: Write a test to verify that when the
fetchUserfunction encounters an error (e.g., API unavailable, user not found), the Promise is correctly rejected with an appropriate error message. - Mocking API Calls: Utilize Jest's mocking capabilities to simulate the behavior of the underlying API calls, allowing you to control the outcomes (resolve or reject) without making actual network requests.
Expected Behavior:
- Your tests should accurately reflect whether the
fetchUserPromise resolves with the correct user object or rejects with a specific error. - The tests should be independent and not rely on external services.
Edge Cases to Consider:
- What happens if the API returns an unexpected data format? (While not explicitly tested here, it's good to keep in mind for real-world scenarios).
- Handling different types of errors returned by the API.
Examples
Let's assume we have a userService module with the following structure (for illustrative purposes, you don't need to implement this, just test it):
// userService.ts (Hypothetical)
interface User {
id: number;
name: string;
}
const mockApi = {
get: async (url: string): Promise<User> => {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (url === '/api/users/1') {
resolve({ id: 1, name: 'Alice' });
} else {
reject(new Error('User not found'));
}
}, 100);
});
},
};
export const userService = {
fetchUser: async (userId: number): Promise<User> => {
try {
const user = await mockApi.get(`/api/users/${userId}`);
return user;
} catch (error) {
throw error; // Re-throw the error to be caught by the caller
}
},
};
Example Test Scenario 1: Successful User Fetch
Input (Implicit): userService.fetchUser(1) is called. The mocked API resolves with { id: 1, name: 'Alice' }.
Expected Output: The fetchUser Promise resolves with an object { id: 1, name: 'Alice' }.
Explanation: The test should assert that when fetchUser is called with a valid userId, the returned Promise resolves with the expected user data.
Example Test Scenario 2: User Not Found
Input (Implicit): userService.fetchUser(99) is called. The mocked API rejects with new Error('User not found').
Expected Output: The fetchUser Promise rejects with an error whose message is 'User not found'.
Explanation: The test should assert that when fetchUser is called with an invalid userId, the returned Promise rejects with the expected error.
Constraints
- Your tests must be written in TypeScript.
- You must use Jest for your testing framework.
- Do not make actual HTTP requests. All API interactions should be mocked.
- The solution should demonstrate clear and concise testing of Promise resolutions and rejections.
Notes
- To mock the
mockApiobject, you will likely need to usejest.mock()andjest.spyOn(). - When testing for Promise rejections, Jest provides specific matchers like
.rejects.toThrow()or.rejects.toMatchObject(). - Consider using
async/awaitwithin your test functions for cleaner Promise handling. - The focus is on testing the asynchronous behavior of
userService.fetchUser.