Monday, December 19, 2022

Batch Apex for Apex Test Class

Batch Apex is used to run large jobs (think thousands or millions of records!) that would exceed normal processing limits. Using Batch Apex, you can process records asynchronously in batches (hence the name, “Batch Apex”) to stay within platform limits. If you have a lot of records to process, for example, data cleansing or archiving, Batch Apex is probably your best solution.

When testing your batch Apex, you can test only one execution of the execute method. You can use the scope parameter of the executeBatch method to limit the number of records passed into the execute method to ensure that you aren’t running into governor limits.

The executeBatch method starts an asynchronous process. This means that when you test batch Apex, you must make certain that the batch job is finished before testing against the results. Use the Test methods startTest and stopTest around the executeBatch method to ensure it finishes before continuing your test. All asynchronous calls made after the startTest method are collected by the system. When stopTest is executed, all asynchronous processes are run synchronously. If you don’t include the executeBatch method within the startTest and stopTest methods, the batch job executes at the end of your test method for Apex saved using Salesforce.com API version 25.0 and later, but not in earlier versions.

Starting with Apex saved using Salesforce.com API version 22.0, exceptions that occur during the execution of a batch Apex job that is invoked by a test method are now passed to the calling test method, and as a result, causes the test method to fail. If you want to handle exceptions in the test method, enclose the code in try and catch statements. You must place the catch block after the stopTest method. Note however that with Apex saved using Salesforce.com API version 21.0 and earlier, such exceptions don’t get passed to the test method and don’t cause test methods to fail.

Example:1

Apex Class:

global class BatchApexTestExample implements Database.Batchable<Sobject>{
    global Database.QueryLocator start(Database.BatchableContext bc){
        return Database.getQueryLocator('select leadsource,rating from lead');
    }
    global void execute(Database.BatchableContext bc,List<Lead> scope){
        for(Lead l : scope){
            if(l.leadsource=='web'){
                l.rating='warm';
            }
        }
        update scope;
    }
    public void finish(Database.BatchableContext bc){
        
    }
}

Test Class

@isTest
private class BatchApexTestExampleTest {
    static testMethod void testme(){
        Lead l = new Lead();
        l.FirstName='Salesforce';
        l.LastName='Codes';
        l.Company='salesforcecodes';
        l.leadsource='Web';
        insert l;
        Test.startTest();
        BatchApexTestExample ba= new BatchApexTestExample();
        Id jobid= Database.executeBatch(ba,5);
        Test.stopTest();
        Lead leads = [select rating from lead where id=:l.Id];
        system.assertEquals('Warm', leads.Rating);
    }
}

Example: 2

Apex Class:

global class AccountUpdate implements Database.Batchable {
     
     global Database.QueryLocator start(Database.BatchableContext BC){
          return Database.getQueryLocator('Select Id, Name From Account');
     }
     global void execute(Database.BatchableContext BC, List  scope){
          for(Account a: scope){
               a.Name += ' Updated';
          }
          update scope;
     }
     global void finish(Database.BatchableContext BC){}

Test Class:

@isTest
private class accListountUpdate {

static testmethod void test() {

// Create test accounts to be updated
// by the batch job.

Account[] accList = new List();
for (Integer i=0;i<200;i++) {
Account m = new Account(Name = 'Account ' + i);
accList.add(m);
}
insert accList;

Test.startTest();
AccountUpdate c = new AccountUpdate();
Database.executeBatch(c);
Test.stopTest();

// Verify accounts updated
Account[] accUpdatedList = [SELECT Id,Name FROM Account];
System.assert(accUpdatedList[0].Name.Contains('Updated'));
}
}

In below code, we are testing above batch class by creating 200 test accounts to make sure that Account Name gets updated by batch apex correctly.

As best practice, you must execute the batch class between Test.StartTest() and Test.StopTest().

We can also load data from static resource to create Accounts using the following syntax.

List ls = Test.loadData(Account.sObjectType,  ‘myResource’);

You must store the static resource (e.g. account.csv) under Salesforce static resources. The supported types of files are text/csv, application/vnd.ms-excel, application/octet-stream, text/plain.

Example : 3

Apex Class:

The following sample class finds all account records that are passed in by the start() method using a QueryLocator and updates the associated contacts with their account’s mailing address. Finally, it sends off an email with the results of the bulk job and, since we are using Database.Stateful to track state, the number of records updated.

public class UpdateContactAddresses implements
    Database.Batchable<sObject>, Database.Stateful {
    // instance member to retain state across transactions
    public Integer recordsProcessed = 0;
    public Database.QueryLocator start(Database.BatchableContext bc) {
        return Database.getQueryLocator(
            'SELECT ID, BillingStreet, BillingCity, BillingState, ' +
            'BillingPostalCode, (SELECT ID, MailingStreet, MailingCity, ' +
            'MailingState, MailingPostalCode FROM Contacts) FROM Account ' +
            'Where BillingCountry = \'USA\''
        );
    }
    public void execute(Database.BatchableContext bc, List<Account> scope){
        // process each batch of records
        List<Contact> contacts = new List<Contact>();
        for (Account account : scope) {
            for (Contact contact : account.contacts) {
                contact.MailingStreet = account.BillingStreet;
                contact.MailingCity = account.BillingCity;
                contact.MailingState = account.BillingState;
                contact.MailingPostalCode = account.BillingPostalCode;
                // add contact to list to be updated
                contacts.add(contact);
                // increment the instance member counter
                recordsProcessed = recordsProcessed + 1;
            }
        }
        update contacts;
    }
    public void finish(Database.BatchableContext bc){
        System.debug(recordsProcessed + ' records processed. Shazam!');
        AsyncApexJob job = [SELECT Id, Status, NumberOfErrors,
            JobItemsProcessed,
            TotalJobItems, CreatedBy.Email
            FROM AsyncApexJob
            WHERE Id = :bc.getJobId()];
        // call some utility to send email
        EmailUtils.sendMessage(job, recordsProcessed);
    }
}

The code should be fairly straightforward but can be a little abstract in reality. Here’s what’s going on in more detail:

  • The start method provides the collection of all records that the execute method will process in individual batches. It returns the list of records to be processed by calling Database.getQueryLocator with a SOQL query. In this case we are simply querying for all Account records with a Billing Country of ‘USA’.
  • Each batch of 200 records is passed in the second parameter of the execute method. The execute method sets each contact’s mailing address to the accounts’ billing address and increments recordsProcessed to track the number of records processed.
  • When the job is complete, the finish method performs a query on the AsyncApexJob object (a table that lists information about batch jobs) to get the status of the job, the submitter’s email address, and some other information. It then sends a notification email to the job submitter that includes the job info and number of contacts updated.
Test Class:
@isTest
private class UpdateContactAddressesTest {
    @testSetup
    static void setup() {
        List<Account> accounts = new List<Account>();
        List<Contact> contacts = new List<Contact>();
        // insert 10 accounts
        for (Integer i=0;i<10;i++) {
            accounts.add(new Account(name='Account '+i,
                billingcity='New York', billingcountry='USA'));
        }
        insert accounts;
        // find the account just inserted. add contact for each
        for (Account account : [select id from account]) {
            contacts.add(new Contact(firstname='first',
                lastname='last', accountId=account.id));
        }
        insert contacts;
    }
    @isTest static void test() {
        Test.startTest();
        UpdateContactAddresses uca = new UpdateContactAddresses();
        Id batchId = Database.executeBatch(uca);
        Test.stopTest();
        // after the testing stops, assert records were updated properly
        System.assertEquals(10, [select count() from contact where MailingCity = 'New York']);
    }
}

No comments:

Post a Comment

Understanding Wire vs Imperative Apex Method Calls in Salesforce Lightning Web Components (LWC)

Understanding Wire vs Imperative Apex Method Calls in Salesforce Lightning Web Components (LWC) Introduction: Salesforce Lightning Web ...