Monday, January 22, 2024

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

Creating a test class for a chaining queueable job in Apex involves understanding how queueable jobs work, how to chain jobs, and how to use the AsyncOptions class to control the behaviour of the jobs. With these tools, you can create robust and reliable asynchronous processes in your Salesforce applications.

In our AccountProcessingQueueable class, we chain jobs by enqueuing a new job within the execute method if there are more accounts to process.

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) {
        List<Account> accountsToProcess = [
            SELECT Id, Name, AnnualRevenue
            FROM Account
            WHERE Id IN :accountIds
            LIMIT :batchSize
        ];
        System.debug(accountsToProcess.size());

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

        update accountsToProcess;

        // If there are more accounts to process, enqueue the next job in the chain
        if (accountIds.size() > batchSize) {
            for (Account account : accountsToProcess) {
                accountIds.remove(account.Id);
            }

            System.enqueueJob(new AccountProcessingQueueable(new Set<Id>(accountIds), batchSize));
        }
    }
}

Creating a Test Class

When creating a test class for a queueable job, we need to create test data, enqueue the job, and then verify the results. The Test.startTest() and Test.stopTest() methods are used to denote the start and end of the test. Between these two methods, we enqueue our job.

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

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

        Test.startTest();
        Set<Id> accountIds = (new Map<Id, SObject>(accounts)).keySet();
        System.enqueueJob(new AccountProcessingQueueable(accountIds, 2), asyncOptions);
        Test.stopTest();

        List<Account> updatedAccounts = [SELECT Id, Description FROM Account WHERE Id IN :accounts];
        for (Account account : updatedAccounts) {
            System.assertEquals('Updated by AccountProcessingQueueable', account.Description);
        }
    }
}

The Rationale Behind the Test Data

In the AccountProcessingQueueableTest class, we create 7 account records. This number is chosen to demonstrate the functionality of the queueable job and its chaining mechanism. The AccountProcessingQueueable job processes accounts in batches, with a batch size of 2. With 7 accounts, we ensure that the job will need to be chained multiple times to process all accounts.

List<Account> accounts = new List<Account>();
for (Integer i = 1; i <= 7; i++) {
    accounts.add(new Account(Name = 'Test ' + i));
}
insert accounts;

Understanding MaximumQueueableStackDepth

The maximumQueueableStackDepth property of the AsyncOptions class is used to limit the depth of the queueable job stack. In this case, it is set to 4. This means that the maximum number of chained jobs that can be added to the stack is 3. If the limit is reached, any further attempt to add a job to the stack will result in a System.AsyncException.

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

Running the Queueable Job

The queueable job is run 4 times before it exits. This is because the batch size is set to 2, and we have 7 accounts to process. The job processes 2 accounts at a time, so it needs to run 4 times to process all 7 accounts. The last run will only process 1 account, as there are no more accounts left to process.

Test.startTest();
Set<Id> accountIds = (new Map<Id, SObject>(accounts)).keySet();
System.enqueueJob(new AccountProcessingQueueable(accountIds, 2), asyncOptions);
Test.stopTest();
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

Friday, January 20, 2023

LWC: Working with custom record forms using lightning-record-edit-form

I was recently working on creating a utility LWC component for displaying Salesforce record data. This component is unique in that it can be used with both standard and custom objects, and t here is no need to create a record form for each object; simply drag and drop this component on any record form page in Lightning App Builder, provide the API name of the object, and supply a few more parameters, and voila! The record form will be generated based on the page layout. By overriding the new and edit buttons, this component may be used to create or change a record.

This component relies on the fact that every record is associated with a page layout, and it requires this information when it is instantiated. If an object has record types, a mapping must be supplied. This component does not currently accept compound fields, but as far as I can tell, it is possible.

I learnt a lot while working on this component, which I'd like to share in this blog.

lightning-record-edit-form can be used for both creating and editing a record. To customize the behaviour of your form when it loads, use the onload attribute to specify event handlers. This is how you gain access to the record

async handleRecordEditFormLoad(event) {
    const record = this.recordId ? event.detail.records[this.recordId] : event.detail.record;

    ...
    ...
}

You can use the below piece of code to display the form once you've retrieved the page layout data (see below).

get sections() {
    return this.layoutSections?.map((layoutSec) => {
        const layoutSection = { ...layoutSec };
        const { layoutColumns } = layoutSection;
        layoutSection.layoutColumns = layoutColumns?.map((layoutColumn, id) => {
            const { layoutItems } = layoutColumn;
            layoutColumn = { ...layoutColumn, id };
            layoutColumn.layoutItems = layoutItems
                ?.map((layoutItem, id) => {
                    layoutItem = { ...layoutItem, id };
                    return layoutItem;
                });
            return layoutColumn;
        });
        return layoutSection;
    });
}

Use getRecord wire adapter to get record’s data.

@wire(getRecord, { recordId: '$recordId', layoutTypes: ['Full'], modes: ['View'] })
    wiredRecord({ error, data }) {
        if (data) {
            this.recordData = data;
            ...
            ...
        }
    }

During record creation, this wire adapter won't fetch recordData for us. As a result, use the onload attribute of lightning-record-edit-form (see above). The recordData will be auto-populated with default values, like OwnerId, so you won’t have to populate it yourself.

When the user is filling up information, use onchange event handler of lightning-input-field to update the record data in memory.

handleInputChange(event) {
    event.preventDefault();
    this.recordData.fields[event.target.dataset.api].value = event.detail.value;
}

If you don't include a lightning-button with type="submit" inside lightning-record-edit-form, this is how you can save the record

handleModalSave() {
    const data = this.recordData;
    this.template.querySelector('lightning-record-edit-form').submit(data);
}

There is a wire adapter getRecordUi that gets layout information, metadata, and data to build UI for one or more records. However, for unknown reasons at this time, it is marked as deprecated. As a result, I had to use metadata API to read the page layout information before the form could be loaded.

Make sure that you check out the UI API Playground provided by Philippe Ozil to study UI APIs and comprehend the type of data JSON gives. Or else use Chrome debugger tools.

Refer SLDS library as much as possible to maintain the aesthetics of Salesforce platform so that you can serve majority of the use cases.

Share This:    Facebook Twitter

Wednesday, August 31, 2022

Tuesday, August 30, 2022

Sending Email in Salesforce using Apex

This is a common snippet to send an email using Salesforce Apex:

Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setToAddresses(new List<String>{'demo@sf.com'});

email.setPlainTextBody('Sample body');
email.setSubject('Sample subject');

List<Messaging.SendEmailResult> results = Messaging.sendEmail(new List<Messaging.SingleEmailMessage>{ email });

for (Messaging.SendEmailResult sr : results) {
    if (!sr.isSuccess()) {
        List<Messaging.SendEmailError> errors = sr.getErrors();
        String errorString = String.join(errors, ', ');
        throw new AuraHandledException(errorString);
    }
}

Sending files with Email

Now assume that we have an order record in Salesforce, and multiple files have been uploaded in Files related list. And we would like to mail a recipient attaching all these files. There is a method setEntityAttachments() on SingleEmailMessage class which accepts an array of ContentVersion Ids.

Make sure that you pass the ContentVersion Ids as a list of String 😐

Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
email.setEntityAttachments(entityAttachmentIds);

Save Email as an activity

What if you would like to mail to a recipient but at the same time save the email record as an activity? For this, use setSaveAsActivity() and setWhatId() methods as below:

Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();
...
...
...
email.setEntityAttachments(new List<String>(contentVersionIds));
email.setSaveAsActivity(true);
email.setWhatId(orderId);

Replace Merge fields in Email Template

Now assume that you have already defined an email template, and there are merge fields (placeholders) both in the body and subject of the email template. When you send the email, these merge fields should get replaced with the Salesforce data from the record. Use renderStoredEmailTemplate() of Messaging class as below:

EmailTemplate emailTemplate = [SELECT Id, Body, Subject FROM EmailTemplate WHERE DeveloperName = :emailTemplateName LIMIT 1];

Messaging.SingleEmailMessage email = new Messaging.SingleEmailMessage();

email.setPlainTextBody(Messaging.renderStoredEmailTemplate(emailTemplate.Id, UserInfo.getUserId(), recordId).plainTextBody);
email.setSubject(Messaging.renderStoredEmailTemplate(emailTemplate.Id, UserInfo.getUserId(), recordId).getSubject());

Remember that executing the renderStoredEmailTemplate() counts toward the SOQL governor limit as one query. This is described more in detail here.

There is a lot of information provided in Salesforce documentation, so do refer the Messaging and SingleEmailMessage classes.

And finally, a full fledged example of sending email using Salesforce Apex: https://gist.github.com/iamsonal/3ccd44b319724f4d03cdb4df0bde54d0

Share This:    Facebook Twitter

Total Pageviews

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