Error Handling
Test how your code handles HTTP errors and edge cases.
HTTP 400 - Bad Request
Test invalid request handling:
@IsTest
static void testBadRequest() {
// Arrange
new HttpMock()
.whenPostOn('/api/users')
.body('{"error": "Email is required"}')
.statusCodeBadRequest()
.mock();
// Act & Assert
Test.startTest();
try {
new UserService().createUser(''); // Empty email
Assert.fail('Expected CalloutException');
} catch (CalloutException e) {
Assert.isTrue(e.getMessage().contains('Email is required'));
}
Test.stopTest();
}HTTP 401 - Unauthorized
Test authentication failures:
@IsTest
static void testUnauthorized() {
// Arrange
new HttpMock()
.whenGetOn('/api/protected/data')
.body('{"error": "Invalid token"}')
.statusCodeUnauthorized()
.mock();
// Act & Assert
Test.startTest();
try {
new ApiService().getProtectedData('invalid-token');
Assert.fail('Expected exception');
} catch (CalloutException e) {
Assert.isTrue(e.getMessage().contains('Unauthorized'));
}
Test.stopTest();
}HTTP 404 - Not Found
Test resource not found scenarios:
@IsTest
static void testNotFound() {
// Arrange
new HttpMock()
.whenGetOn('/api/users/999')
.body('{"error": "User not found"}')
.statusCodeNotFound()
.mock();
// Act
Test.startTest();
User user = new UserService().getUser('999');
Test.stopTest();
// Assert
Assert.isNull(user, 'User should be null for 404 response');
}HTTP 500 - Internal Server Error
Test server error handling:
@IsTest
static void testServerError() {
// Arrange
new HttpMock()
.whenGetOn('/api/users')
.body('{"error": "Internal server error"}')
.statusCodeInternalServerError()
.mock();
// Act & Assert
Test.startTest();
try {
new UserService().getUsers();
Assert.fail('Expected CalloutException');
} catch (CalloutException e) {
Assert.isTrue(e.getMessage().contains('500'));
}
Test.stopTest();
}HTTP 503 - Service Unavailable
Test service downtime:
@IsTest
static void testServiceUnavailable() {
// Arrange
new HttpMock()
.whenGetOn('/api/users')
.body('{"error": "Service temporarily unavailable"}')
.statusCodeServiceUnavailable()
.header('Retry-After', '120')
.mock();
// Act
Test.startTest();
ApiResponse response = new UserService().getUsersWithRetry();
Test.stopTest();
// Assert
Assert.isTrue(response.shouldRetry);
Assert.areEqual(120, response.retryAfter);
}Retry Logic
Test automatic retry mechanisms:
@IsTest
static void testRetryMechanism() {
// Arrange - first two calls fail, third succeeds
new HttpMock()
.whenGetOn('/api/unstable')
.body('{"error": "Timeout"}')
.statusCodeGatewayTimeout()
.whenGetOn('/api/unstable')
.body('{"error": "Timeout"}')
.statusCodeGatewayTimeout()
.whenGetOn('/api/unstable')
.body('{"success": true}')
.statusCodeOk()
.mock();
// Act
Test.startTest();
ApiResponse response = new ApiService().callWithRetry();
Test.stopTest();
// Assert
Assert.isTrue(response.success);
Assert.areEqual(3, HttpMock.getRequestCount('GET', '/api/unstable'));
}Empty Response Body
Test handling of responses with no body:
@IsTest
static void testEmptyResponse() {
// Arrange
new HttpMock()
.whenDeleteOn('/api/users/123')
.statusCodeNoContent()
.mock();
// Act
Test.startTest();
Boolean deleted = new UserService().deleteUser('123');
Test.stopTest();
// Assert
Assert.isTrue(deleted);
}Malformed JSON
Test handling of invalid response data:
@IsTest
static void testMalformedJson() {
// Arrange
new HttpMock()
.whenGetOn('/api/broken')
.body('{"invalid": json}') // Invalid JSON
.statusCodeOk()
.mock();
// Act & Assert
Test.startTest();
try {
new ApiService().parseResponse();
Assert.fail('Expected JSON parsing exception');
} catch (Exception e) {
Assert.isTrue(e instanceof JSONException);
}
Test.stopTest();
}Rate Limiting
Test rate limit handling:
@IsTest
static void testRateLimit() {
// Arrange
new HttpMock()
.whenGetOn('/api/users')
.body('{"error": "Rate limit exceeded"}')
.statusCode(429) // Too Many Requests
.header('X-RateLimit-Remaining', '0')
.header('X-RateLimit-Reset', '1640000000')
.mock();
// Act
Test.startTest();
ApiResponse response = new UserService().getUsers();
Test.stopTest();
// Assert
Assert.isTrue(response.rateLimitExceeded);
Assert.areEqual(1640000000, response.rateLimitReset);
}Timeout Simulation
Combine with Test.stopTest() timing to test timeouts:
@IsTest
static void testTimeout() {
// Arrange
new HttpMock()
.whenGetOn('/api/slow')
.body('{"data": "finally"}')
.statusCodeGatewayTimeout()
.mock();
// Act
Test.startTest();
ApiService service = new ApiService();
service.setTimeout(1000); // 1 second timeout
try {
service.callSlowEndpoint();
Assert.fail('Expected timeout exception');
} catch (Exception e) {
Assert.isTrue(e.getMessage().contains('timeout'));
}
Test.stopTest();
}Fallback Behavior
Test fallback to cached or default data:
@IsTest
static void testFallbackOnError() {
// Arrange
new HttpMock()
.whenGetOn('/api/users')
.body('{"error": "Service unavailable"}')
.statusCodeServiceUnavailable()
.mock();
// Act
Test.startTest();
List<User> users = new UserService().getUsersWithFallback();
Test.stopTest();
// Assert
Assert.isNotNull(users);
Assert.areEqual(0, users.size(), 'Should return empty list as fallback');
}Multiple Error Scenarios
Test different error responses from the same endpoint:
@IsTest
static void testMultipleErrorTypes() {
// Arrange
new HttpMock()
.whenPostOn('/api/users')
.body('{"error": "Bad Request"}')
.statusCodeBadRequest()
.whenPostOn('/api/users')
.body('{"error": "Unauthorized"}')
.statusCodeUnauthorized()
.whenPostOn('/api/users')
.body('{"success": true}')
.statusCodeCreated()
.mock();
// Act
Test.startTest();
UserService service = new UserService();
// First call - bad request
try {
service.createUser('invalid-data');
Assert.fail('Expected exception');
} catch (CalloutException e) {
Assert.isTrue(e.getMessage().contains('Bad Request'));
}
// Second call - unauthorized
try {
service.createUser('no-auth');
Assert.fail('Expected exception');
} catch (CalloutException e) {
Assert.isTrue(e.getMessage().contains('Unauthorized'));
}
// Third call - success
String userId = service.createUser('valid-data');
Assert.isNotNull(userId);
Test.stopTest();
}Custom Error Codes
Test custom HTTP status codes:
@IsTest
static void testCustomStatusCode() {
// Arrange
new HttpMock()
.whenGetOn('/api/deprecated')
.body('{"message": "API endpoint deprecated"}')
.statusCode(410) // Gone
.header('Sunset', 'Sat, 31 Dec 2024 23:59:59 GMT')
.mock();
// Act & Assert
Test.startTest();
try {
new ApiService().callDeprecatedEndpoint();
Assert.fail('Expected exception for deprecated endpoint');
} catch (Exception e) {
Assert.isTrue(e.getMessage().contains('deprecated'));
}
Test.stopTest();
}Best Practices
Test All Error Codes - Don't just test the happy path; verify error handling
Use Realistic Errors - Mock error responses that match what the real API returns
Verify Error Messages - Check that your code properly parses and handles error details
Test Recovery - Verify retry logic, fallbacks, and graceful degradation
Document Edge Cases - Comment why you expect certain exceptions
