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);
}
}
@isTest— marks the mock class.public class PaymentGatewaySuccessMock implements HttpCalloutMock {— Module 36's mocking interface.public HttpResponse respond(HttpRequest request) {— the required method.HttpResponse response = new HttpResponse();— a fabricated response.response.setStatusCode(200);— a successful status.response.setBody('{"transactionId": "txn_123"}');— a fabricated success body.return response;— hands it back.}— closes the method.}— closes the mock class.- (blank line)
@isTest— marks the test class.private class ProcessPaymentJobTest {— the test class.@isTest— marks the test method.static void successfulPaymentMarksOrderPaid() {— states what's verified.Order__c order = new Order__c(Total__c = 500, Payment_Token__c = 'tok_abc');— test data.insert order;— saves it.- (blank line)
Test.setMock(HttpCalloutMock.class, new PaymentGatewaySuccessMock());— swaps in the fabricated success response.- (blank line)
Test.startTest();— the governor-limit boundary.System.enqueueJob(new ProcessPaymentJob(order.Id, 500, 'tok_abc', 1));— enqueues the job under test.Test.stopTest(); // forces the Queueable job to actually run before this line returns— Module 31's boundary, doing double duty here.- (blank line)
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.System.assertEquals('Paid', updated.Status__c);— a specific expected status.System.assertEquals('txn_123', updated.Payment_Reference__c);— a specific expected reference.}— closes the test method.}— 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);
}
@isTest— marks the mock class.public class PaymentGatewayDeclineMock implements HttpCalloutMock {— a second, separate mock.public HttpResponse respond(HttpRequest request) {— the required method.HttpResponse response = new HttpResponse();— a fabricated response.response.setStatusCode(402);— the gateway's decline status.response.setBody('{"reason": "Insufficient funds"}');— a fabricated decline reason.return response;— hands it back.}— closes the method.}— closes the mock class.- (blank line)
@isTest— marks the test method.static void declinedPaymentMarksOrderDeclined() {— states what's verified.Order__c order = new Order__c(Total__c = 500, Payment_Token__c = 'tok_abc');— test data.insert order;— saves it.- (blank line)
Test.setMock(HttpCalloutMock.class, new PaymentGatewayDeclineMock());— swaps in the decline mock.- (blank line)
Test.startTest();— the boundary.System.enqueueJob(new ProcessPaymentJob(order.Id, 500, 'tok_abc', 1));— enqueues the job.Test.stopTest();— forces it to run.- (blank line)
Order__c updated = [SELECT Status__c, Decline_Reason__c FROM Order__c WHERE Id = :order.Id];— re-queries the record.System.assertEquals('Payment Declined', updated.Status__c);— confirms the correct final status.System.assertEquals('Insufficient funds', updated.Decline_Reason__c);— confirms the specific reason was captured.}— 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);
}
@isTest— marks the mock class.public class PaymentGatewayErrorMock implements HttpCalloutMock {— a third mock, simulating a technical failure.public HttpResponse respond(HttpRequest request) {— the required method.HttpResponse response = new HttpResponse();— a fabricated response.response.setStatusCode(500);— an error status, neither success nor decline.return response;— hands it back.}— closes the method.}— closes the mock class.- (blank line)
@isTest— marks the test method.static void thirdFailedAttemptGivesUpWithPaymentError() {— states the exact boundary under test.Order__c order = new Order__c(Total__c = 500, Payment_Token__c = 'tok_abc');— test data.insert order;— saves it.- (blank line)
Test.setMock(HttpCalloutMock.class, new PaymentGatewayErrorMock());— swaps in the error mock.- (blank line)
Test.startTest();— the boundary.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.Test.stopTest();— forces it to run.- (blank line)
Order__c updated = [SELECT Status__c FROM Order__c WHERE Id = :order.Id];— re-queries the record.System.assertEquals('Payment Error', updated.Status__c);— confirms the job gave up rather than chaining a fourth attempt.}— 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.
Test with Mocks Quiz
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.