/** * @description Test class for external API callouts using mock framework * Demonstrates HttpCalloutMock and WebServiceMock patterns. * @author {{Author}} * @created {{Date}} */ @IsTest private class {{ClassName}}CalloutTest { // ═══════════════════════════════════════════════════════════════════════════ // MOCK IMPLEMENTATIONS // ═══════════════════════════════════════════════════════════════════════════ /** * @description Mock for successful HTTP response (200 OK) */ private class SuccessMock implements HttpCalloutMock { public HttpResponse respond(HttpRequest req) { HttpResponse res = new HttpResponse(); res.setStatusCode(200); res.setStatus('OK'); res.setHeader('Content-Type', 'application/json'); res.setBody(JSON.serialize(new Map{ 'success' => true, 'data' => new Map{ 'id' => '12345', 'status' => 'completed', 'timestamp' => Datetime.now().format() } })); return res; } } /** * @description Mock for error HTTP response (500 Internal Server Error) */ private class ErrorMock implements HttpCalloutMock { private Integer statusCode; private String errorMessage; public ErrorMock(Integer statusCode, String errorMessage) { this.statusCode = statusCode; this.errorMessage = errorMessage; } public HttpResponse respond(HttpRequest req) { HttpResponse res = new HttpResponse(); res.setStatusCode(this.statusCode); res.setStatus('Error'); res.setHeader('Content-Type', 'application/json'); res.setBody(JSON.serialize(new Map{ 'success' => false, 'error' => this.errorMessage })); return res; } } /** * @description Mock for timeout simulation */ private class TimeoutMock implements HttpCalloutMock { public HttpResponse respond(HttpRequest req) { // Simulate timeout by throwing CalloutException throw new CalloutException('Read timed out'); } } /** * @description Mock that validates request parameters */ private class ValidatingMock implements HttpCalloutMock { private String expectedEndpoint; private String expectedMethod; public ValidatingMock(String endpoint, String method) { this.expectedEndpoint = endpoint; this.expectedMethod = method; } public HttpResponse respond(HttpRequest req) { Assert.areEqual(expectedMethod, req.getMethod(), 'HTTP method should be ' + expectedMethod); Assert.isTrue(req.getEndpoint().contains(expectedEndpoint), 'Endpoint should contain ' + expectedEndpoint); HttpResponse res = new HttpResponse(); res.setStatusCode(200); res.setBody('{"validated": true}'); return res; } } // ═══════════════════════════════════════════════════════════════════════════ // SUCCESS TESTS // ═══════════════════════════════════════════════════════════════════════════ /** * @description Tests successful API call returns expected data */ @IsTest static void testCallout_Success_ReturnsData() { // GIVEN Test.setMock(HttpCalloutMock.class, new SuccessMock()); // WHEN Test.startTest(); // TODO: Replace with your actual service call // {{ClassName}}Response response = {{ClassName}}.callExternalAPI('test-param'); HttpRequest req = new HttpRequest(); req.setEndpoint('https://api.example.com/test'); req.setMethod('GET'); HttpResponse res = new Http().send(req); Test.stopTest(); // THEN Assert.areEqual(200, res.getStatusCode(), 'Should return 200 OK'); Map responseBody = (Map) JSON.deserializeUntyped(res.getBody()); Assert.areEqual(true, responseBody.get('success'), 'Response should indicate success'); Assert.isNotNull(responseBody.get('data'), 'Response should contain data'); } /** * @description Tests POST request with body */ @IsTest static void testCallout_PostWithBody_Success() { // GIVEN Test.setMock(HttpCalloutMock.class, new SuccessMock()); Map requestBody = new Map{ 'name' => 'Test Record', 'value' => 100 }; // WHEN Test.startTest(); HttpRequest req = new HttpRequest(); req.setEndpoint('https://api.example.com/create'); req.setMethod('POST'); req.setHeader('Content-Type', 'application/json'); req.setBody(JSON.serialize(requestBody)); HttpResponse res = new Http().send(req); Test.stopTest(); // THEN Assert.areEqual(200, res.getStatusCode(), 'POST should succeed'); } // ═══════════════════════════════════════════════════════════════════════════ // ERROR HANDLING TESTS // ═══════════════════════════════════════════════════════════════════════════ /** * @description Tests handling of 400 Bad Request */ @IsTest static void testCallout_BadRequest_HandlesGracefully() { // GIVEN Test.setMock(HttpCalloutMock.class, new ErrorMock(400, 'Invalid request parameters')); // WHEN Test.startTest(); HttpRequest req = new HttpRequest(); req.setEndpoint('https://api.example.com/test'); req.setMethod('GET'); HttpResponse res = new Http().send(req); Test.stopTest(); // THEN Assert.areEqual(400, res.getStatusCode(), 'Should return 400'); Map errorBody = (Map) JSON.deserializeUntyped(res.getBody()); Assert.areEqual(false, errorBody.get('success'), 'Should indicate failure'); Assert.isNotNull(errorBody.get('error'), 'Should contain error message'); } /** * @description Tests handling of 500 Internal Server Error */ @IsTest static void testCallout_ServerError_HandlesGracefully() { // GIVEN Test.setMock(HttpCalloutMock.class, new ErrorMock(500, 'Internal server error')); // WHEN Test.startTest(); HttpRequest req = new HttpRequest(); req.setEndpoint('https://api.example.com/test'); req.setMethod('GET'); HttpResponse res = new Http().send(req); Test.stopTest(); // THEN Assert.areEqual(500, res.getStatusCode(), 'Should return 500'); } /** * @description Tests handling of timeout exception */ @IsTest static void testCallout_Timeout_ThrowsException() { // GIVEN Test.setMock(HttpCalloutMock.class, new TimeoutMock()); // WHEN/THEN Test.startTest(); try { HttpRequest req = new HttpRequest(); req.setEndpoint('https://api.example.com/slow'); req.setMethod('GET'); new Http().send(req); Assert.fail('Expected CalloutException was not thrown'); } catch (CalloutException e) { Assert.isTrue(e.getMessage().containsIgnoreCase('timeout'), 'Exception should mention timeout: ' + e.getMessage()); } Test.stopTest(); } /** * @description Tests handling of 401 Unauthorized */ @IsTest static void testCallout_Unauthorized_HandlesGracefully() { // GIVEN Test.setMock(HttpCalloutMock.class, new ErrorMock(401, 'Invalid or expired token')); // WHEN Test.startTest(); HttpRequest req = new HttpRequest(); req.setEndpoint('https://api.example.com/protected'); req.setMethod('GET'); HttpResponse res = new Http().send(req); Test.stopTest(); // THEN Assert.areEqual(401, res.getStatusCode(), 'Should return 401 Unauthorized'); } // ═══════════════════════════════════════════════════════════════════════════ // REQUEST VALIDATION TESTS // ═══════════════════════════════════════════════════════════════════════════ /** * @description Verifies correct endpoint and method are used */ @IsTest static void testCallout_ValidatesRequestParameters() { // GIVEN Test.setMock(HttpCalloutMock.class, new ValidatingMock('/api/v1/resource', 'POST')); // WHEN Test.startTest(); HttpRequest req = new HttpRequest(); req.setEndpoint('https://api.example.com/api/v1/resource'); req.setMethod('POST'); req.setBody('{"test": true}'); HttpResponse res = new Http().send(req); Test.stopTest(); // THEN - Mock validates; test passes if no assertion failure Assert.areEqual(200, res.getStatusCode(), 'Request should be valid'); } // ═══════════════════════════════════════════════════════════════════════════ // ASYNC CALLOUT TESTS (@future, Queueable) // ═══════════════════════════════════════════════════════════════════════════ /** * @description Tests @future method with callout * Note: Must use Test.startTest/stopTest to execute @future */ @IsTest static void testFutureCallout_ExecutesSuccessfully() { // GIVEN Test.setMock(HttpCalloutMock.class, new SuccessMock()); // WHEN - Call @future method Test.startTest(); // TODO: Replace with your actual @future method // {{ClassName}}.makeAsyncCallout('test-param'); Test.stopTest(); // Forces @future to execute // THEN - Verify results (may need to query for side effects) // TODO: Add assertions based on what @future method does } /** * @description Tests Queueable with callout */ @IsTest static void testQueueableCallout_ExecutesSuccessfully() { // GIVEN Test.setMock(HttpCalloutMock.class, new SuccessMock()); // WHEN Test.startTest(); // TODO: Replace with your actual Queueable // System.enqueueJob(new {{ClassName}}Queueable('param')); Test.stopTest(); // Forces Queueable to execute // THEN - Verify results // TODO: Add assertions } // ═══════════════════════════════════════════════════════════════════════════ // MULTI-CALLOUT TESTS // ═══════════════════════════════════════════════════════════════════════════ /** * @description Mock for multiple sequential callouts */ private class MultiCalloutMock implements HttpCalloutMock { private Integer callCount = 0; public HttpResponse respond(HttpRequest req) { callCount++; HttpResponse res = new HttpResponse(); res.setStatusCode(200); res.setBody('{"callNumber": ' + callCount + '}'); return res; } } /** * @description Tests multiple callouts in sequence (limit: 100 per transaction) */ @IsTest static void testMultipleCallouts_AllSucceed() { // GIVEN Test.setMock(HttpCalloutMock.class, new MultiCalloutMock()); // WHEN - Make multiple callouts Test.startTest(); List responses = new List(); for (Integer i = 0; i < 5; i++) { HttpRequest req = new HttpRequest(); req.setEndpoint('https://api.example.com/call/' + i); req.setMethod('GET'); responses.add(new Http().send(req)); } Test.stopTest(); // THEN Assert.areEqual(5, responses.size(), 'Should complete 5 callouts'); for (HttpResponse res : responses) { Assert.areEqual(200, res.getStatusCode(), 'Each callout should succeed'); } } }