Monday, January 22, 2024

Salesforce Apex: Creating an Apex Test Class for a Chaining Queueable Job

Queueable jobs in Salesforce are a powerful way to handle asynchronous processing, allowing you to chain jobs for scalability and efficiency. This blog post explores how to implement a chaining Queueable job in Apex, test it effectively, and use the AsyncOptions class to control job behavior.

The AccountProcessingQueueable class processes a set of Account records in batches and chains additional jobs if more records remain to be processed. Below is the implementation:

public class AccountProcessingQueueable implements Queueable {
    private Set<Id> accountIds;
    private Integer batchSize;

    public AccountProcessingQueueable(Set<Id> accountIds, Integer batchSize) {
        this.accountIds = accountIds;
        this.batchSize = batchSize;
    }

    public void execute(QueueableContext context) {
        // Query accounts to process, limited by batchSize
        List<Account> accountsToProcess = [
            SELECT Id, Name, AnnualRevenue
            FROM Account
            WHERE Id IN :accountIds
            LIMIT :batchSize
        ];
        System.debug('Processing ' + accountsToProcess.size() + ' accounts');

        // Perform complex calculations and updates
        for (Account account : accountsToProcess) {
            account.Description = 'Updated by AccountProcessingQueueable';
        }

        // Update accounts if there are any to process
        if (!accountsToProcess.isEmpty()) {
            update accountsToProcess;
        }

        // Remove processed account IDs from the set
        for (Account account : accountsToProcess) {
            accountIds.remove(account.Id);
        }

        // If there are more accounts to process, enqueue the next job
        if (!accountIds.isEmpty()) {
            System.enqueueJob(new AccountProcessingQueueable(accountIds, batchSize));
        }
    }
}

Creating a Test Class

Testing Queueable jobs requires creating test data, enqueuing the job, and verifying the results. The Test.startTest() and Test.stopTest() methods are used to ensure asynchronous jobs execute synchronously within the test context, allowing you to validate their behavior.

Below is the test class for the AccountProcessingQueueable class:

@IsTest
public with sharing class AccountProcessingQueueableTest {
    @IsTest
    public static void testQueueable() {
        // Create test data: 7 accounts
        List<Account> accounts = new List<Account>();
        for (Integer i = 1; i <= 7; i++) {
            accounts.add(new Account(Name = 'Test ' + i));
        }
        insert accounts;

        // Prepare account IDs
        Set<Id> accountIds = new Map<Id, SObject>(accounts).keySet();

        // Set up AsyncOptions to limit chaining depth
        AsyncOptions asyncOptions = new AsyncOptions();
        asyncOptions.maximumQueueableStackDepth = 4;

        // Start test context and enqueue the job
        Test.startTest();
        System.enqueueJob(new AccountProcessingQueueable(accountIds, 2), asyncOptions);
        Test.stopTest();

        // Verify results
        List<Account> updatedAccounts = [SELECT Id, Description FROM Account WHERE Id IN :accountIds];
        for (Account account : updatedAccounts) {
            System.assertEquals('Updated by AccountProcessingQueueable', account.Description, 
                'Account description should be updated by the Queueable job');
        }
    }
}

The Rationale Behind the Test Data

In the AccountProcessingQueueableTest class, we create 7 account records to demonstrate the chaining mechanism of the Queueable job. With a batch size of 2, the job processes accounts in batches, requiring multiple chained executions to handle all 7 accounts. Specifically:

  • The first job processes accounts 1–2 (2 accounts).
  • The second job processes accounts 3–4 (2 accounts).
  • The third job processes accounts 5–6 (2 accounts).
  • The fourth job processes account 7 (1 account).

This setup ensures that the chaining logic is thoroughly tested, including the handling of partial batches in the final execution.


Understanding maximumQueueableStackDepth

The AsyncOptions class, introduced in Salesforce, allows you to control the behavior of Queueable jobs, including the maximumQueueableStackDepth property. This property limits the number of chained Queueable jobs that can be enqueued in a single execution context.

Key Points About maximumQueueableStackDepth:

  • The default value is 50, meaning up to 50 chained jobs can be enqueued in a single transaction.
  • Setting maximumQueueableStackDepth to a lower value (e.g., 4) restricts the number of chained jobs to 3 additional jobs beyond the initial job (total of 4 jobs in the chain).
  • If the limit is exceeded, Salesforce throws a System.AsyncException with a message indicating that the maximum stack depth has been reached.

In the test class, we set maximumQueueableStackDepth to 4 to demonstrate how to control chaining depth:

AsyncOptions asyncOptions = new AsyncOptions();
asyncOptions.maximumQueueableStackDepth = 4;

Running the Queueable Job

With 7 accounts and a batch size of 2, the Queueable job executes 4 times to process all records:

  • Job 1: Processes 2 accounts (remaining: 5).
  • Job 2: Processes 2 accounts (remaining: 3).
  • Job 3: Processes 2 accounts (remaining: 1).
  • Job 4: Processes 1 account (remaining: 0).

The Test.stopTest() method ensures that all chained jobs complete before the test context ends, allowing you to verify the results immediately.

Share This:    Facebook Twitter

Sunday, January 21, 2024

Apex: Get List of SObject records by Ids

The getSobjectListById() method is a powerful utility function that can greatly simplify the task of grouping SObject records by a specific field. By improving code performance and readability, this method can help you write more efficient and maintainable Apex code.

public static Map<Id, List<SObject>> getSobjectListById(String key, List<SObject> incomingList) {
    Map<Id, List<SObject>> returnValues = new Map<Id, List<SObject>>();
    for (SObject current : incomingList) {
        if (current.get(key) != null) {
            Id currentId = (Id) current.get(key);
            if (!returnValues.containsKey(currentId)) {
                returnValues.put(currentId, new List<SObject>());
            }
            returnValues.get(currentId).add(current);
        }
    }
    return returnValues;
}

This utility function takes a field name (key) and a list of SObject records as parameters. It returns a map where the keys are the unique IDs from the specified field, and the values are lists of SObject records that have the same field value.

Let's consider a real-life scenario where getSobjectListById() can be used. Suppose you are working on a Salesforce project where you need to send a customized email to each Account's Contacts. The email content is based on the specific Account's details.

First, you would query all the Contacts and their related Account details. Then, you would need to group these Contacts based on their AccountId. This is where getSobjectListById() comes into play. You can use this method to create a map where the key is the AccountId and the value is a list of Contacts related to that Account.

Here's how you can do it:

List<Contact> contactList = [SELECT Id, Name, AccountId, Account.Name FROM Contact];
Map<Id, List<SObject>> accountContactsMap = Utils.getSobjectListById('AccountId', contactList);

Now, accountContactsMap contains a list of Contacts for each AccountId. You can iterate over this map to send a customized email to each Account's Contacts.

Share This:    Facebook Twitter

Total Pageviews

509951

My Social Profiles

View Sonal's profile on LinkedIn

Tags

__proto__ $Browser Access Grants Accessor properties Admin Ajax AllowsCallouts Apex Apex Map Apex Sharing AssignmentRuleHeader AsyncApexJob Asynchronous Auth Provider AWS Callbacks Connected app constructor Cookie CPU Time CSP Trusted Sites CSS Custom settings CustomLabels Data properties Database.Batchable Database.BatchableContext Database.query Describe Result Destructuring Dynamic Apex Dynamic SOQL Einstein Analytics enqueueJob Enterprise Territory Management Enumeration escapeSingleQuotes featured Flows geolocation getGlobalDescribe getOrgDefaults() getPicklistValues getRecordTypeId() getRecordTypeInfosByName() getURLParameters Google Maps Governor Limits hasOwnProperty() Heap Heap Size IIFE Immediately Invoked Function Expression Interview questions isCustom() Javascript Javascript Array jsForce Lightning Lightning Components Lightning Events lightning-record-edit-form lightning:combobox lightning:icon lightning:input lightning:select LockerService Lookup LWC Manual Sharing Map Modal Module Pattern Named Credentials NodeJS OAuth Object.freeze() Object.keys() Object.preventExtensions() Object.seal() Organization Wide Defaults Override PDF Reader Performance performance.now() Permission Sets Picklist Platform events Popup Postman Primitive Types Profiles Promise propertyIsEnumerable() prototype Query Selectivity Queueable Record types Reference Types Regex Regular Expressions Relationships Rest API Rest Operator Revealing Module Pattern Role Hierarchy Salesforce Salesforce Security Schema.DescribeFieldResult Schema.DescribeSObjectResult Schema.PicklistEntry Schema.SObjectField Schema.SObjectType Security Service Components Shadow DOM Sharing Sharing Rules Singleton Slots SOAP API SOAP Web Services SOQL SOQL injection Spread Operator Star Rating stripInaccessible svg svgIcon Synchronous this Token Triggers uiObjectInfoApi Upload Files VSCode Web Services XHR
Scroll To Top