Advanced 30 min read

Test with HttpCalloutMock

By the end of this lesson, you'll be able to:

  • Test the success path with a mock returning a valid response
  • Test the non-200 error path with a mock
  • Test the CalloutException path using Test.setMock with a throwing mock

Prerequisites: "Handle Errors and Timeouts"

Testing the success path

@isTest
public class CurrencyApiSuccessMock implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest request) {
        HttpResponse response = new HttpResponse();
        response.setStatusCode(200);
        response.setBody('{"convertedAmount": 5.43, "rate": 0.054, "timestamp": "2026-08-06T10:00:00Z"}');
        return response;
    }
}

@isTest
private class CurrencyConversionServiceTest {
    @isTest
    static void convertReturnsCorrectAmount() {
        Test.setMock(HttpCalloutMock.class, new CurrencyApiSuccessMock());

        Test.startTest();
        Decimal result = new CurrencyConversionService().convert(100, 'USD');
        Test.stopTest();

        System.assertEquals(5.43, result);
    }
}
  1. @isTest — marks the mock class.
  2. public class CurrencyApiSuccessMock implements HttpCalloutMock { — Module 36's mocking interface.
  3. public HttpResponse respond(HttpRequest request) { — the required method.
  4. HttpResponse response = new HttpResponse(); — a fabricated response.
  5. response.setStatusCode(200); — a successful status.
  6. response.setBody('{"convertedAmount": 5.43, "rate": 0.054, "timestamp": "2026-08-06T10:00:00Z"}'); — a fabricated body matching Lesson 1's contract exactly.
  7. return response; — hands it back.
  8. } — closes the method.
  9. } — closes the mock class.
  10. (blank line)
  11. @isTest — marks the test class.
  12. private class CurrencyConversionServiceTest { — the test class.
  13. @isTest — marks the test method.
  14. static void convertReturnsCorrectAmount() { — states what's verified.
  15. Test.setMock(HttpCalloutMock.class, new CurrencyApiSuccessMock()); — swaps in the fabricated response.
  16. (blank line)
  17. Test.startTest(); — the governor-limit boundary.
  18. Decimal result = new CurrencyConversionService().convert(100, 'USD'); — calls the real service.
  19. Test.stopTest(); — closes the boundary.
  20. (blank line)
  21. System.assertEquals(5.43, result); — a specific expected value, matching the mock's fabricated convertedAmount.
  22. } — closes the test method.
  23. } — closes the test class.

Exactly Module 36's closing lesson, applied to this real service — a specific fabricated response, a specific expected value, both matching Lesson 1's contract precisely.

Testing the non-200 error path

@isTest
public class CurrencyApiErrorMock implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest request) {
        HttpResponse response = new HttpResponse();
        response.setStatusCode(400);
        response.setBody('{"error": "Unrecognized currency code"}');
        return response;
    }
}

@isTest
static void convertThrowsOnBadCurrencyCode() {
    Test.setMock(HttpCalloutMock.class, new CurrencyApiErrorMock());

    try {
        new CurrencyConversionService().convert(100, 'XYZ');
        System.assert(false, 'Expected a CurrencyConversionException');
    } catch (CurrencyConversionException e) {
        System.assert(e.getMessage().contains('400'));
    }
}
  1. @isTest — marks the mock class.
  2. public class CurrencyApiErrorMock implements HttpCalloutMock { — a second, separate mock for the error scenario.
  3. public HttpResponse respond(HttpRequest request) { — the required method.
  4. HttpResponse response = new HttpResponse(); — a fabricated response.
  5. response.setStatusCode(400); — this time, an error status.
  6. response.setBody('{"error": "Unrecognized currency code"}'); — a plausible error body, matching Lesson 1's named edge case.
  7. return response; — hands it back.
  8. } — closes the method.
  9. } — closes the mock class.
  10. (blank line)
  11. @isTest — marks the test method.
  12. static void convertThrowsOnBadCurrencyCode() { — states the exact scenario under test.
  13. Test.setMock(HttpCalloutMock.class, new CurrencyApiErrorMock()); — swaps in the error mock.
  14. (blank line)
  15. try { — Module 20's exception-testing shape.
  16. new CurrencyConversionService().convert(100, 'XYZ'); — expected to throw, given the mocked 400.
  17. System.assert(false, 'Expected a CurrencyConversionException'); — fails the test if no exception was thrown.
  18. } catch (CurrencyConversionException e) { — catches the expected exception type.
  19. System.assert(e.getMessage().contains('400')); — confirms the message reflects the actual failing status.
  20. } — closes the try/catch.
  21. } — closes the test method.

This confirms Lesson 5's error-handling branch fires correctly on exactly the kind of realistic failure named back in Lesson 1's design — an unrecognized currency code.

Testing a CalloutException with a throwing mock

@isTest
public class CurrencyApiTimeoutMock implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest request) {
        throw new CalloutException('Read timed out');
    }
}

@isTest
static void convertHandlesNetworkFailure() {
    Test.setMock(HttpCalloutMock.class, new CurrencyApiTimeoutMock());

    try {
        new CurrencyConversionService().convert(100, 'USD');
        System.assert(false, 'Expected a CurrencyConversionException');
    } catch (CurrencyConversionException e) {
        System.assert(e.getMessage().contains('Could not reach'));
    }
}
  1. @isTest — marks the mock class.
  2. public class CurrencyApiTimeoutMock implements HttpCalloutMock { — a third mock, simulating a network failure.
  3. public HttpResponse respond(HttpRequest request) { — the required method.
  4. throw new CalloutException('Read timed out'); — throws directly, rather than returning a response at all.
  5. } — closes the method.
  6. } — closes the mock class.
  7. (blank line)
  8. @isTest — marks the test method.
  9. static void convertHandlesNetworkFailure() { — states the exact scenario under test.
  10. Test.setMock(HttpCalloutMock.class, new CurrencyApiTimeoutMock()); — swaps in the throwing mock.
  11. (blank line)
  12. try { — the same testing shape as the other two tests.
  13. new CurrencyConversionService().convert(100, 'USD'); — expected to throw, given the mocked failure.
  14. System.assert(false, 'Expected a CurrencyConversionException'); — fails the test if nothing was thrown.
  15. } catch (CurrencyConversionException e) { — confirms the catch (CalloutException e) inside convert actually caught and re-wrapped it.
  16. System.assert(e.getMessage().contains('Could not reach')); — confirms the specific re-wrapped message.
  17. } — closes the try/catch.
  18. } — closes the test method.

A mock's respond() method can throw directly, simulating exactly the "no response received at all" scenario Lesson 5 distinguished from a non-200 response — confirming the try/catch around new Http().send(request) actually catches it and wraps it correctly.

Exercise

Write a mock returning a 500 status, and a test confirming convert() throws a CurrencyConversionException mentioning "500" for that response.

Show hint

Follow the same pattern as the 400 mock and test.

Salesforce: ○ Not connected Connect
APEX

Execution Results

Run your code to see the result.

Test with HttpCalloutMock Quiz

1. How does a mock class simulate a genuine network failure (CalloutException), rather than an error response?

Log in to submit the quiz and save your score.

My Notes

Log in to keep private notes on this lesson.

Questions about this lesson

No questions yet — be the first to ask.

Log in to ask a question about this lesson.

Summary

This lesson tests all three paths built across this module — success, a non-200 error, and a genuine callout failure — following Module 36's testing-integrations lesson directly.