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);
    }
}

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);
}

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);
}

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.