Skip to main content

Mastering Batch Apex in Salesforce: Building and Testing Efficient Data Processing Jobs

 



Batch Apex is designed for handling large-scale operations that involve processing thousands or even millions of records—tasks that would normally exceed Salesforce’s standard execution limits. By dividing data into smaller sets, or batches, this approach allows processing to occur asynchronously, helping you stay within platform limits. If your use case involves actions like data cleanup, mass updates, or archiving, Batch Apex is often the most efficient choice.

When writing tests for Batch Apex, only a single execution of the execute method can be directly tested. To manage system resources effectively and avoid governor limit issues, you can specify the scope parameter in the executeBatch method, which controls how many records are processed at a time.

The executeBatch call runs asynchronously, meaning the process continues in the background after it starts. Therefore, during testing, it’s essential to ensure that the batch process completes before verifying the results. To do this, wrap the executeBatch call between the startTest and stopTest methods. Any asynchronous calls initiated after startTest are queued by Salesforce, and when stopTest runs, those queued operations are executed synchronously. If you omit startTest and stopTest, the batch job won’t execute until the very end of the test method in Apex versions 25.0 and later, and not at all in earlier versions.

From API version 22.0 onward, any exceptions that occur during a batch job invoked within a test are propagated back to the test method, causing the test to fail. To manage these exceptions properly, wrap your code in a try-catch block, placing the catch section after the stopTest method. In older versions (21.0 and earlier), such exceptions don’t bubble up to the test method, so they won’t cause it to fail.


global class ContactTypeUpdaterBatch implements Database.Batchable<SObject>, Database.Stateful {


Batch Class

    // Query all Contacts that need updating

    global Database.QueryLocator start(Database.BatchableContext bc) {

        return Database.getQueryLocator([

            SELECT Id, Industry, Type 

            FROM Contact

            WHERE Industry != null

        ]);

    }


    // Process each batch of records

    global void execute(Database.BatchableContext bc, List<Contact> scope) {

        List<Contact> contactsToUpdate = new List<Contact>();


        for (Contact con : scope) {

            String industryValue = con.Industry;

            String currentType = con.Type;


            // Build the new Type value

            String newTypeValue = industryValue + ' Professional';


            // Update if blank or not already prefixed correctly

            if (currentType == null || currentType != newTypeValue) {

                con.Type = newTypeValue;

                contactsToUpdate.add(con);

            }

        }


        if (!contactsToUpdate.isEmpty()) {

            update contactsToUpdate;

        }

    }


    // Finish method for any post-processing or logging

    global void finish(Database.BatchableContext bc) {

        System.debug('Contact Type update batch completed successfully.');

    }

}


How to Run It

You can execute the batch from Anonymous Apex in the Developer Console or VS Code:

ContactTypeUpdaterBatch batch = new ContactTypeUpdaterBatch(); Database.executeBatch(batch, 200); // 200 = batch size


How It Works

  • start() → Queries all Contacts that have a non-null Industry.

  • execute() → For each batch, prefixes the Industry to “Professional” (e.g., Technology Professional).

  • finish() → Logs completion or can be extended to send notifications.



Test Class


@isTest private class ContactTypeUpdaterBatchTest { @isTest static void testContactTypeUpdaterBatch() { // Setup Test Data List<Contact> testContacts = new List<Contact>{ new Contact(FirstName = 'John', LastName = 'Doe', Industry = 'Technology', Type = null), new Contact(FirstName = 'Jane', LastName = 'Smith', Industry = 'Finance', Type = 'Professional'), new Contact(FirstName = 'Alex', LastName = 'Brown', Industry = 'Healthcare', Type = 'Healthcare Professional'), new Contact(FirstName = 'Mary', LastName = 'Johnson', Industry = null, Type = 'Consultant') // Should not be updated }; insert testContacts; // Start the Test Context Test.startTest(); // Execute the batch ContactTypeUpdaterBatch batch = new ContactTypeUpdaterBatch(); Database.executeBatch(batch, 200); Test.stopTest(); // Ensures all async code (batch) finishes execution // Query updated data List<Contact> updatedContacts = [ SELECT Id, Industry, Type FROM Contact WHERE Id IN :testContacts ]; // Validate results using System.asserts for (Contact con : updatedContacts) { if (con.Industry == 'Technology') { System.assertEquals('Technology Professional', con.Type, 'Contact Type should be prefixed with Industry for Technology industry'); } else if (con.Industry == 'Finance') { System.assertEquals('Finance Professional', con.Type, 'Contact Type should be prefixed with Industry for Finance industry'); } else if (con.Industry == 'Healthcare') { System.assertEquals('Healthcare Professional', con.Type, 'Type already correct, should remain unchanged'); } else if (con.Industry == null) { System.assertEquals('Consultant', con.Type, 'Contacts without Industry should not be updated'); } } } }

Explanation of the Test Class

Creating Test Data

We create four sample Contact records to cover different scenarios:

  • Contact 1: Has Industry but no Type → Should get “Technology Professional.”

  • Contact 2: Has Type “Professional” → Should be updated to “Finance Professional.”

  • Contact 3: Already correctly prefixed → Should remain unchanged.

  • Contact 4: Has no Industry → Should not be updated.

This ensures full code coverage and logical validation.


Running the Batch Inside a Test Context

  • Test.startTest() — Marks the beginning of the test’s execution phase.

  • Database.executeBatch(batch, 200) — Runs the batch with a batch size of 200 records.

  • Test.stopTest() — Forces all asynchronous operations (like Batch Apex) to finish before continuing.

Without startTest and stopTest, the batch might not complete before your assertions run.


Query Updated Records

After the batch runs, we query the same contacts to check if the Type field has been correctly updated based on the logic.


Assertions (System.assertEquals)

We verify that:

  • Contacts with a valid Industry have their Type correctly prefixed.

  • Contacts without Industry remain unchanged.

  • Contacts already matching the intended format are not altered.

These assertions confirm both functional correctness and that the batch’s conditional logic works as expected.


Code Coverage

This test class:

  • Covers 100% of the batch class code (all branches executed).

  • Validates all possible logical conditions.

  • Uses best testing practices — no hard-coded IDs, clear assertions, and test isolation.


Comments