Advanced 30 min read

Test with Mocks

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

  • Test the success, decline, and error paths using HttpCalloutMock
  • Test the retry logic specifically, confirming it stops at the attempt limit
  • Apply Module 31 and Module 38's testing patterns to asynchronous, mocked code together

Prerequisites: "Secure Sensitive Data"

Testing the success path

@isTest
public class PaymentGatewaySuccessMock implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest request) {
        HttpResponse response = new HttpResponse();
        response.setStatusCode(200);
        response.setBody('{"transactionId": "txn_123"}');
        return response;
    }
}

@isTest
private class ProcessPaymentJobTest {
    @isTest
    static void successfulPaymentMarksOrderPaid() {
        Order__c order = new Order__c(Total__c = 500, Payment_Token__c = 'tok_abc');
        insert order;

        Test.setMock(HttpCalloutMock.class, new PaymentGatewaySuccessMock());

        Test.startTest();
        System.enqueueJob(new ProcessPaymentJob(order.Id, 500, 'tok_abc', 1));
        Test.stopTest(); // forces the Queueable job to actually run before this line returns

        Order__c updated = [SELECT Status__c, Payment_Reference__c FROM Order__c WHERE Id = :order.Id];
        System.assertEquals('Paid', updated.Status__c);
        System.assertEquals('txn_123', updated.Payment_Reference__c);
    }
}
  1. @isTest — marks the mock class.
  2. public class PaymentGatewaySuccessMock 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('{"transactionId": "txn_123"}'); — a fabricated success body.
  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 ProcessPaymentJobTest { — the test class.
  13. @isTest — marks the test method.
  14. static void successfulPaymentMarksOrderPaid() { — states what's verified.
  15. Order__c order = new Order__c(Total__c = 500, Payment_Token__c = 'tok_abc'); — test data.
  16. insert order; — saves it.
  17. (blank line)
  18. Test.setMock(HttpCalloutMock.class, new PaymentGatewaySuccessMock()); — swaps in the fabricated success response.
  19. (blank line)
  20. Test.startTest(); — the governor-limit boundary.
  21. System.enqueueJob(new ProcessPaymentJob(order.Id, 500, 'tok_abc', 1)); — enqueues the job under test.
  22. Test.stopTest(); // forces the Queueable job to actually run before this line returns — Module 31's boundary, doing double duty here.
  23. (blank line)
  24. Order__c updated = [SELECT Status__c, Payment_Reference__c FROM Order__c WHERE Id = :order.Id]; — re-queries the record after the job has run.
  25. System.assertEquals('Paid', updated.Status__c); — a specific expected status.
  26. System.assertEquals('txn_123', updated.Payment_Reference__c); — a specific expected reference.
  27. } — closes the test method.
  28. } — closes the test class.

Test.stopTest() (Module 31) does double duty here — it forces the Queueable job to run synchronously to completion, meaning the assertion afterward can safely check the result of the async work, not just that it was enqueued.

Testing the decline path

@isTest
public class PaymentGatewayDeclineMock implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest request) {
        HttpResponse response = new HttpResponse();
        response.setStatusCode(402);
        response.setBody('{"reason": "Insufficient funds"}');
        return response;
    }
}

@isTest
static void declinedPaymentMarksOrderDeclined() {
    Order__c order = new Order__c(Total__c = 500, Payment_Token__c = 'tok_abc');
    insert order;

    Test.setMock(HttpCalloutMock.class, new PaymentGatewayDeclineMock());

    Test.startTest();
    System.enqueueJob(new ProcessPaymentJob(order.Id, 500, 'tok_abc', 1));
    Test.stopTest();

    Order__c updated = [SELECT Status__c, Decline_Reason__c FROM Order__c WHERE Id = :order.Id];
    System.assertEquals('Payment Declined', updated.Status__c);
    System.assertEquals('Insufficient funds', updated.Decline_Reason__c);
}
  1. @isTest — marks the mock class.
  2. public class PaymentGatewayDeclineMock implements HttpCalloutMock { — a second, separate mock.
  3. public HttpResponse respond(HttpRequest request) { — the required method.
  4. HttpResponse response = new HttpResponse(); — a fabricated response.
  5. response.setStatusCode(402); — the gateway's decline status.
  6. response.setBody('{"reason": "Insufficient funds"}'); — a fabricated decline reason.
  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 declinedPaymentMarksOrderDeclined() { — states what's verified.
  13. Order__c order = new Order__c(Total__c = 500, Payment_Token__c = 'tok_abc'); — test data.
  14. insert order; — saves it.
  15. (blank line)
  16. Test.setMock(HttpCalloutMock.class, new PaymentGatewayDeclineMock()); — swaps in the decline mock.
  17. (blank line)
  18. Test.startTest(); — the boundary.
  19. System.enqueueJob(new ProcessPaymentJob(order.Id, 500, 'tok_abc', 1)); — enqueues the job.
  20. Test.stopTest(); — forces it to run.
  21. (blank line)
  22. Order__c updated = [SELECT Status__c, Decline_Reason__c FROM Order__c WHERE Id = :order.Id]; — re-queries the record.
  23. System.assertEquals('Payment Declined', updated.Status__c); — confirms the correct final status.
  24. System.assertEquals('Insufficient funds', updated.Decline_Reason__c); — confirms the specific reason was captured.
  25. } — closes the test method.

This confirms 402 gets routed correctly through Lesson 2's status-code branching all the way to the final Order__c status — the full chain, exercised end to end.

Testing that the retry limit is respected

@isTest
public class PaymentGatewayErrorMock implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest request) {
        HttpResponse response = new HttpResponse();
        response.setStatusCode(500);
        return response;
    }
}

@isTest
static void thirdFailedAttemptGivesUpWithPaymentError() {
    Order__c order = new Order__c(Total__c = 500, Payment_Token__c = 'tok_abc');
    insert order;

    Test.setMock(HttpCalloutMock.class, new PaymentGatewayErrorMock());

    Test.startTest();
    System.enqueueJob(new ProcessPaymentJob(order.Id, 500, 'tok_abc', 3)); // already the 3rd attempt
    Test.stopTest();

    Order__c updated = [SELECT Status__c FROM Order__c WHERE Id = :order.Id];
    System.assertEquals('Payment Error', updated.Status__c);
}
  1. @isTest — marks the mock class.
  2. public class PaymentGatewayErrorMock implements HttpCalloutMock { — a third mock, simulating a technical failure.
  3. public HttpResponse respond(HttpRequest request) { — the required method.
  4. HttpResponse response = new HttpResponse(); — a fabricated response.
  5. response.setStatusCode(500); — an error status, neither success nor decline.
  6. return response; — hands it back.
  7. } — closes the method.
  8. } — closes the mock class.
  9. (blank line)
  10. @isTest — marks the test method.
  11. static void thirdFailedAttemptGivesUpWithPaymentError() { — states the exact boundary under test.
  12. Order__c order = new Order__c(Total__c = 500, Payment_Token__c = 'tok_abc'); — test data.
  13. insert order; — saves it.
  14. (blank line)
  15. Test.setMock(HttpCalloutMock.class, new PaymentGatewayErrorMock()); — swaps in the error mock.
  16. (blank line)
  17. Test.startTest(); — the boundary.
  18. System.enqueueJob(new ProcessPaymentJob(order.Id, 500, 'tok_abc', 3)); // already the 3rd attempt — starts directly at the retry limit, rather than simulating all three attempts.
  19. Test.stopTest(); — forces it to run.
  20. (blank line)
  21. Order__c updated = [SELECT Status__c FROM Order__c WHERE Id = :order.Id]; — re-queries the record.
  22. System.assertEquals('Payment Error', updated.Status__c); — confirms the job gave up rather than chaining a fourth attempt.
  23. } — closes the test method.

Starting at attemptNumber = 3 (the limit) directly, rather than simulating all three attempts, tests Lesson 4's boundary condition precisely — confirming the job gives up and records Payment Error instead of chaining a fourth attempt.

Exercise

Write a test confirming that an attempt with attemptNumber = 1 and a 500 error response results in the job chaining a retry (you can confirm this by checking a new AsyncApexJob exists after Test.stopTest, since chaining during tests only allows one level).

Show hint

Query AsyncApexJob after Test.stopTest() and check for a queued job.

APEX

Test with Mocks Quiz

1. Why does Test.stopTest() matter for testing ProcessPaymentJob specifically?

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 every path built across this module — success, decline, error-with-retry, and the retry limit itself — combining Module 36's mocking with Module 31's async-testing habits.