Introduction
Salesforce Batch Apex is an essential feature for processing large volumes of data asynchronously. Running a batch job with a custom dataset during development can pose challenges. This is especially true if you want to avoid altering the deployed code.
Also, consider a scenario where you’re working in a developer sandbox. The dataset needed by the batch class’s start method query is unavailable. Despite this, you still need to test your batch class. It is essential to achieve the necessary test coverage for deployment to higher environments.
In this blog post, we’ll explore a straightforward technique. You can run an existing batch class with a custom dataset. This is done using a constructor-based query. Additionally, we’ll demonstrate how to test a batch class effectively, even when the start method query cannot retrieve data.
Running an Existing Batch Class with a Custom Dataset
In real-world scenarios, developers often need to clean or manipulate data in your salesforce org’s without deploying new batch logic. By using the constructor of a batch class, you can accept a custom query. This approach allows you to pass a dataset dynamically when invoking the batch from Anonymous Apex. Here’s how it works:
- Default Behavior: The batch class uses a pre-defined query during scheduled runs.
- Custom Dataset: When a custom query is passed via the constructor, the batch processes the dataset specified in the query.
This approach ensures flexibility and avoids frequent deployments, empowering developers to use existing batch logic efficiently.
Implementation
Here’s an example of a batch class designed to handle both default and custom datasets:
/**
* Custom Batch Apex Example
* This class demonstrates how to create a batch Apex class that can accept a custom query.
* It processes Account records and updates their names.
* It includes a constructor to accept a custom query and a default query if none is provided.
*/
public class CustomBatchExample implements Database.Batchable<SObject> {
// Declare a variable to hold the custom query
private String query;
/**
* Default constructor
* This constructor initializes the batch with a default query that selects Accounts created in the last 30 days.
* If no custom query is provided, this default query will be used.
* This constructor is useful for scenarios where the user does not need to specify a custom query.
* It allows the batch to run with a predefined query without requiring any parameters.
*/
public CustomBatchExample() {
this.query = '';
}
/**
* Constructor with custom query
* This constructor allows the user to pass a custom SOQL query to the batch class.
* This query will override the default query if provided.
* If no custom query is provided, the batch will use a default query that selects Accounts created in the last 30 days.
* @param customQuery The custom SOQL query to be used in the batch process.
*/
public CustomBatchExample(String customQuery) {
this.query = customQuery;
}
/**
* start method is used to define the scope of records to be processed in the batch.
* It returns a QueryLocator that defines the scope of records to be processed.
* If a custom query is provided, it uses that; otherwise, it defaults to a query that selects Accounts created in the last 30 days.
* @param bc The context for the batchable job.
* @return A QueryLocator that defines the records to be processed in the batch.
*/
public Database.QueryLocator start(Database.BatchableContext bc) {
// Use the custom query if provided, otherwise use the default query
if (String.isNotBlank(query)) {
return Database.getQueryLocator(query);
} else {
return Database.getQueryLocator('SELECT Id, Name FROM Account WHERE CreatedDate = LAST_N_DAYS:30 And (NOT Name LIKE \'Test%\')');
}
}
/**
* execute method is used to process each batch of records.
* It receives a list of Account records in the scope and updates their names.
* @param bc The context for the batchable job.
* @param scope The list of Account records to be processed in this batch.
*/
public void execute(Database.BatchableContext bc, List<Account> scope) {
System.debug('scope+++'+scope);
List<Account> lstAccToUpdate = new List<Account>();
for (Account record : scope) {
// Process each record
Account acc = new Account();
acc.Id = record.Id;
acc.Name = record.Name + ' - Updated';
lstAccToUpdate.add(acc);
}
if(!lstAccToUpdate.isEmpty()) {
update lstAccToUpdate;
}
}
/**
* finish method is called after all batches have been processed.
* It can be used for any finalization logic, such as sending notifications or logging.
* @param bc The context for the batchable job.
*/
public void finish(Database.BatchableContext bc) {
// Finalization logic
System.debug('Batch process completed.');
}
}
Executing the Batch with Custom Data
Run the batch class with a custom query using Anonymous Apex:
String customQuery = 'SELECT Id, Name FROM Account WHERE Industry = \'Technology\'';
Database.executeBatch(new CustomBatchExample(customQuery));
Test Class
@isTest
public class CustomBatchExampleTest {
@isTest
static void testBatchWithDefaultQuery() {
// Create test Accounts (let's create 3)
List<Account> testAccounts = new List<Account>();
for (Integer i = 0; i < 3; i++) {
testAccounts.add(new Account(Name = 'Account ' + i));
}
insert testAccounts;
Test.startTest();
// Batch size is 3, which is >= number of records returned
Database.executeBatch(new CustomBatchExample(), 3);
Test.stopTest();
// Verify the accounts were updated
List<Account> updatedAccounts = [SELECT Name FROM Account WHERE Id IN :testAccounts];
for (Account acc : updatedAccounts) {
System.assert(acc.Name.endsWith(' - Updated'), 'Account name should be updated');
}
}
@isTest
static void testBatchWithCustomQuery() {
// Create test Accounts (let's create 2)
List<Account> testAccounts = new List<Account>();
for (Integer i = 0; i < 2; i++) {
testAccounts.add(new Account(Name = 'Custom Account ' + i));
}
insert testAccounts;
String customQuery = 'SELECT Id, Name FROM Account WHERE Name LIKE \'Custom Account%\'';
Test.startTest();
// Batch size is 2, which is >= number of records returned
Database.executeBatch(new CustomBatchExample(customQuery), 2);
Test.stopTest();
// Verify the accounts were updated
List<Account> updatedAccounts = [SELECT Name FROM Account WHERE Id IN :testAccounts];
for (Account acc : updatedAccounts) {
System.assert(acc.Name.endsWith(' - Updated'), 'Account name should be updated');
}
}
}
With this flexible batch processing approach, developers can execute Batch Apex classes with custom datasets for development and testing purposes. The technique eliminates the need for additional deployments while ensuring data quality and process reliability.
Handling Custom Datasets in Test Class Without Modifying the Batch Class
When testing a batch class, you might encounter a scenario. The query in the start method is not able to get any records. This happens because of filter conditions in the query. You can’t create test data in test class to match these filter conditions in the test class.
For example, lastmodifieddate <= Today. So, Instead of modifying the batch class, you can use Database.executeBatch with a null context in the test class to cover the logic effectively.
When the batch’s start method uses filters or logic that you cannot satisfy or simulate in a test context.Suppose your batch class’s start method queries records based on fields that are:
- Read-only or system-generated (e.g.,
LastModifiedDate) - Populated by triggers, flows, or external integrations (not directly settable in test context)
- Dependent on data that cannot be created in a test context (e.g., records owned by a specific user type, or with a specific status set by a process)
In these cases, creating test data that matches the query is impossible. As a result, the batch would process zero records in your test.
Solution
You can directly call the execute method in your test and pass in a list of records you control. This allows you to:
- Test the logic inside
execute - Ensure your batch logic works as expected, even if the
startmethod can’t return test data
Example
Let’s consider the above batch class example. If we create the test method below, it will not cover the execute method of the batch class when run. We are creating an account with a name starting from ‘Test’. This name will not match the start method filters.
@isTest
static void testBatchWithNonMatchingData() {
List<Account> testAccounts = new List<Account>();
for (Integer i = 0; i < 3; i++) {
testAccounts.add(new Account(Name = 'Test Account ' + i));
}
insert testAccounts;
Test.startTest();
// Batch size is 3, which is >= number of records returned
Database.executeBatch(new CustomBatchExample(), 3);
Test.stopTest();
// Verify the accounts were updated
List<Account> updatedAccounts = [SELECT Name FROM Account WHERE Id IN :testAccounts];
System.debug('updatedAccounts+++'+updatedAccounts);
for (Account acc : updatedAccounts) {
System.assert(!acc.Name.endsWith(' - Updated'), 'Account name should not be updated');
}
}

Here’s how you can fix it:
- Mock the Dataset in Test Class: Create test data according to your need.
- Skip the
startMethod Logic: UseDatabase.executeBatchwith anullcontext to bypass thestartmethod and directly test theexecuteandfinishmethods.
@isTest
static void testBatchWithNonMatchingDataButPassedFromExecuteDirectly() {
// Create test Accounts (let's create 3)
List<Account> testAccounts = new List<Account>();
for (Integer i = 0; i < 3; i++) {
testAccounts.add(new Account(Name = 'Test Account ' + i));
}
insert testAccounts;
List<Account> updatedAccounts2 = [SELECT Name FROM Account WHERE Id IN :testAccounts];
Test.startTest();
Database.BatchableContext bc = null;
CustomBatchExample batchInstance = new CustomBatchExample();
batchInstance.execute(bc, updatedAccounts2);
Database.executeBatch(new CustomBatchExample(), 3);
Test.stopTest();
// Verify the accounts were updated
List<Account> updatedAccounts = [SELECT Name FROM Account WHERE Id IN :testAccounts];
for (Account acc : updatedAccounts) {
System.assert(acc.Name.endsWith(' - Updated'), 'Account name should be updated');
}
}
We created the account with a name starting from ‘Test’ based on the above test method. Now we can pass this non-matching data directly to the batch class execute. This covers the batch class execute method.

This technique allows you to cover the execute and finish methods of a batch class in a test class. It does not rely on data availability for the start method. By leveraging Database.executeBatch with null or invoking batch methods directly, you ensure comprehensive test coverage while adhering to Salesforce best practices.
Directly calling execute is useful when:
- The
startmethod’s query can’t be satisfied in a test context - You want to test the
executelogic with controlled data
Tip: Always document why you are bypassing the normal batch flow in your test for clarity.


Leave a reply to shantanu kumar Cancel reply