Testing Error Scenarios
Unit Testing Error Handling
describe('API Error Handling', () => {
it('should refresh token on TOKEN_EXPIRED', async () => {
// Mock expired token response
fetchMock.mockResponseOnce(
JSON.stringify({
error: 'Token expired',
error_code: 'TOKEN_EXPIRED',
timestamp: new Date().toISOString()
}),
{ status: 401 }
);
// Mock successful refresh
fetchMock.mockResponseOnce(
JSON.stringify({
access_token: 'new_token',
refresh_token: 'new_refresh_token'
})
);
// Mock successful retry
fetchMock.mockResponseOnce(
JSON.stringify({ id: 'user_123' })
);
const result = await apiRequestWithRefresh('/api/user');
expect(result.id).toBe('user_123');
});
it('should handle rate limiting with backoff', async () => {
// Mock rate limit response
fetchMock.mockResponseOnce('', {
status: 429,
headers: { 'Retry-After': '2' }
});
// Mock successful retry
fetchMock.mockResponseOnce(
JSON.stringify({ users: [] })
);
const result = await fetchWithBackoff('/api/service/users');
expect(result.users).toEqual([]);
});
});
Integration Testing
Test error scenarios in integration tests:
describe('Service API Integration', () => {
it('should return 403 for insufficient permissions', async () => {
const apiKey = await createAPIKey(['read:users']); // No write permission
const response = await fetch(`${API_URL}/api/service/users`, {
method: 'POST',
headers: {
'X-API-Key': apiKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({ email: 'test@example.com' })
});
expect(response.status).toBe(403);
const error = await response.json();
expect(error.error_code).toBe('FORBIDDEN');
});
});
Debugging Tips
1. Use Timestamps for Correlation
Error timestamps help correlate client-side errors with server logs:
const error = await response.json();
console.error(`Error at ${error.timestamp}:`, error.error);
// Search server logs around this timestamp
2. Check Rate Limit Headers
Monitor rate limit headers to prevent hitting limits:
const response = await fetch(url, options);
console.log('Rate Limit:', {
limit: response.headers.get('X-RateLimit-Limit'),
remaining: response.headers.get('X-RateLimit-Remaining'),
reset: new Date(parseInt(response.headers.get('X-RateLimit-Reset')) * 1000)
});
3. Enable Request Logging
Log all API requests for debugging:
async function apiRequest(url, options) {
const startTime = Date.now();
console.log('API Request:', { url, method: options.method });
try {
const response = await fetch(url, options);
const duration = Date.now() - startTime;
console.log('API Response:', {
url,
status: response.status,
duration: `${duration}ms`
});
return response;
} catch (error) {
const duration = Date.now() - startTime;
console.error('API Request Failed:', {
url,
duration: `${duration}ms`,
error: error.message
});
throw error;
}
}
4. Use Request IDs
Generate unique request IDs for tracing:
function generateRequestId() {
return `req_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
}
async function apiRequest(url, options) {
const requestId = generateRequestId();
options.headers['X-Request-ID'] = requestId;
console.log(`[${requestId}] Request:`, url);
try {
const response = await fetch(url, options);
console.log(`[${requestId}] Response:`, response.status);
return response;
} catch (error) {
console.error(`[${requestId}] Error:`, error.message);
throw error;
}
}