如何对调用外部API的Node.js控制器/函数进行单元测试?
Hey there! Let's break down your questions one by one to clear up the confusion around testing your createUser function that calls an external API.
You're absolutely right here. When you call the real external API in a test, that's an integration test (or end-to-end/flow test). Unit tests are meant to isolate and validate your code's logic—not the interaction with external services. Integration tests check if your code plays nicely with the real API (network connectivity, parameter formatting, response handling across the full chain), but they're slow, dependent on the API being available, and not ideal for catching logic bugs in your own code.
Mocking the external API isn't just about testing "success vs failure"—it's about controlling every possible scenario to validate how your code behaves. Here's what you should be testing:
- Successful responses: Verify your function correctly parses the API's return data, transforms it if needed, and returns the right value to its caller.
- Different failure scenarios: Don't stop at generic failures—test specific cases like HTTP 400 (bad request, e.g., missing parameters), 401 (unauthorized), 500 (server error), or network timeouts. This ensures your function handles each error type appropriately (throws meaningful errors, retries if configured, etc.).
- Parameter validation: Use mocks to confirm your function sends the exact right parameters to the API. For example: Did you pass the
namefield in the correct place? Is the request method (POST) and headers (likeContent-Type: application/json) set properly? - Edge case simulations: Even if the external API handles edge cases (like missing
name), you can mock those scenarios to test your code's part in the flow—did you pass the incomplete data correctly? Does your function handle the API's error response for that edge case gracefully?
Short answer: Yes, absolutely. Here's why:
- Your code might mess up parameter passing: Even if the API handles missing
name, what if your code accidentally sendsusernameinstead ofname? A unit test with a mock would catch this immediately, without needing to hit the real API. - Your response handling logic is yours to own: The API might return a specific error for missing
name, but does your function propagate that error correctly to the caller? Or does it swallow it and return an unclear message? Unit tests validate this logic. - Isolation and speed: Unit tests run in milliseconds, don't depend on the external API being up, and can be run constantly during development. Integration tests are slower and should be used alongside unit tests, not as a replacement.
- Regression protection: When you modify
createUserlater (e.g., add a new parameter), unit tests will quickly tell you if you broke existing logic—no need to wait for an integration test suite to run.
Example Unit Test (using Jest)
Let's say your createUser function looks something like this:
const request = require('request'); const PATH = require('./config/somewhere'); export const createUser = async (userData) => { return new Promise((resolve, reject) => { request.post({ url: `${PATH}/users`, json: userData }, (err, res, body) => { if (err) return reject(err); if (res.statusCode !== 201) return reject(new Error(`API failed with status ${res.statusCode}`)); resolve(body); }); }); };
A unit test suite for this would look like:
const request = require('request'); const { createUser } = require('./your-module'); const PATH = require('./config/somewhere'); // Mock the entire request module jest.mock('request'); describe('createUser', () => { afterEach(() => { jest.clearAllMocks(); }); it('calls the API with correct params and resolves on success', async () => { const mockUserData = { name: 'Alice', email: 'alice@test.com' }; const mockResponse = { id: 123, ...mockUserData }; // Mock the post method to simulate success request.post.mockImplementation((options, callback) => { expect(options.url).toBe(`${PATH}/users`); expect(options.json).toEqual(mockUserData); callback(null, { statusCode: 201 }, mockResponse); }); const result = await createUser(mockUserData); expect(result).toEqual(mockResponse); expect(request.post).toHaveBeenCalledTimes(1); }); it('rejects with correct error when API returns 400 (missing name)', async () => { const mockUserData = { email: 'bob@test.com' }; // No name const mockApiError = { message: 'Name is required' }; request.post.mockImplementation((options, callback) => { expect(options.json).toEqual(mockUserData); // Verify we sent the incomplete data callback(null, { statusCode: 400 }, mockApiError); }); await expect(createUser(mockUserData)).rejects.toThrow('API failed with status 400'); }); it('rejects with network error when request fails', async () => { const mockError = new Error('Network timeout'); request.post.mockImplementation((options, callback) => { callback(mockError, null, null); }); await expect(createUser({ name: 'Charlie' })).rejects.toThrow(mockError); }); });
This suite tests all critical paths of your code, independent of the real external API.
内容的提问来源于stack exchange,提问作者user9728810

