Skip to main content

How to test Schedulable Batchable

Have you ever thought, how to test scheduled batch? If your answer is no, because Test.startTest() and Test.stopTest() would do the job for you, you are not entirely right and you may find this article interesting.

Clean the mess!

For start let's define some "more-or-less" real life scenario for those, who want to follow in their orgs. We have a custom object Log (API Name: Log__c, Name Type: Auto Number), that we use to log exceptions, integrations and some other events worth saving. Custom object has several fields containing information like error message and stack trace, but it is not really important for our purpose. Unfortunately our predecessors (or anyone else we can easily blame) didn’t do their work properly creating immense number of bugs in the system and our object is overloaded with logged exceptions and takes too much of the storage.

Fixing all the bugs in the system will take months, so we need to take care of the storage first. Our 2 000 hard-working Oompa Loompas… 2 000 hard working users are daily generating more records, than we can delete in one transaction. We also need to store logs for at least 7 days. Let’s quickly write some Batch Apex, that will be run every night out of working hours.

The code is pretty clear, so I will not spend any more time commenting it.

global class DeleteLogs implements Schedulable, Database.Batchable<SObject> {
 
    private DateTime DELETION_DEADLINE;
    
    global Database.QueryLocator start (Database.BatchableContext BC) {
        DELETION_DEADLINE = DateTime.now().addDays(-6);
        return Database.getQueryLocator('SELECT Id FROM Log__c WHERE CreatedDate < :DELETION_DEADLINE');
    }
    
    global void execute(Database.BatchableContext BC, List<Log__c> scope) {
        delete scope;       
    }
    
    global void finish(Database.BatchableContext BC) {
    }
    
    global void execute(SchedulableContext SC) {
        Database.executeBatch(new DeleteLogs());
    }
}

Alright, although the class is a bit ugly, it should work. We are going to celebrate it with 100 % code coverage. First we will try to do it in one method. We will schedule our batch inbetween Test.startTest() and Test.stopTest().

@IsTest
private class DeleteLogsTest {

    private static final Integer TOTAL_RECORDS = 3;
    private static final Integer RECORDS_TO_DELETE = 2;
    private static final Integer REMAINING_RECORDS = TOTAL_RECORDS - RECORDS_TO_DELETE;  
    
    // Let's create Log records, but only some of them older than 7 days
    @testSetup
    static void setup() {
        // Create logs
        List<Log__c> logs = new List<Log__c>();
        for (Integer i = 0; i < TOTAL_RECORDS; i++) {
            logs.add(new Log__c());
        }
        insert logs;
        
        // Set createdDate 7 days back for some of the records
        for (Integer i = 0; i < RECORDS_TO_DELETE; i++) {
            // "Midnights tests" won't cause any issue here, because we are not seeking
            // records exactly 7 days old, but at least 7 days old
            Test.setCreatedDate(logs[i].Id, DateTime.now().addDays(-7));
        }                
    }    
    
    // Let's test both Batchable and Schedulable at once
    @IsTest
    private static void testWillNotWork() {
        Test.StartTest();
            System.schedule('Log Deletion', '0 0 1 * * ?', new DeleteLogs());
        Test.StopTest();
        
        List<Log__c> remainingLogs = [SELECT Id FROM Log__c];
        System.assertEquals(REMAINING_RECORDS, remainingLogs.size(), 'Incorrect number of records was deleted.');
    }
}

Proper test

Assertation failed, because no record was deleted. But why, when documentation says "All asynchronous calls made after the startTest method are collected by the system. When stopTest is executed, all asynchronous processes are run synchronously."? Because system doesn't collect asynchronous processes nested in other asynchronous processes. It works on one level only.

So, how can we achieve our goal of 100 % code coverage? We need to test Batchable and Schedulable part separately. In testBatchable method we are going to test deletion of records and in testSchedulable we are only testing, if Schedulable part executes Batchable.

@IsTest
private class DeleteLogsTest {

    private static final Integer TOTAL_RECORDS = 3;
    private static final Integer RECORDS_TO_DELETE = 2;
    private static final Integer REMAINING_RECORDS = TOTAL_RECORDS - RECORDS_TO_DELETE;  
    
    // Let's create Log records, but only some of them older than 7 days
    @testSetup
    private static void setup() {
        // Create logs
        List<Log__c> logs = new List<Log__c>();
        for (Integer i = 0; i < TOTAL_RECORDS; i++) {
            logs.add(new Log__c());
        }
        insert logs;
        
        // Set createdDate 7 days back for some of the records
        for (Integer i = 0; i < RECORDS_TO_DELETE; i++) {
            // "Midnights tests" won't cause any issue here, because we are not seeking
            // records exactly 7 days old, but at least 7 days old
            Test.setCreatedDate(logs[i].Id, DateTime.now().addDays(-7));
        }                
    }    
       
    @IsTest
    static void testBatchable() {
        Test.StartTest();
            Database.executeBatch(new DeleteLogs());
        Test.StopTest();
        
        List<Log__c> remainingLogs = [SELECT Id FROM Log__c];
        System.assertEquals(REMAINING_RECORDS, remainingLogs.size(), 'Incorrect number of records was deleted.');        
    }
    
    @IsTest
    private static void testSchedulable() {        
        Test.startTest();
            System.schedule('Delete Logs Schedulable', '0 0 1 * * ?', new DeleteLogs());
        Test.stopTest();

        List<AsyncApexJob> batchJobs = [SELECT Id, Status FROM AsyncApexJob WHERE ApexClass.Name = 'DeleteLogs' AND JobType = 'BatchApex'];        
        System.assertEquals(1, batchJobs.size(), 'Unexpected number of batch jobs ran: ' + batchJobs);
        System.assertEquals('Queued', batchJobs[0].Status, 'Job planned with an unexpected status.');
    }        
}

Run Tests… 1, 2… Yes! We did it again!

Looking for an experienced Salesforce Architect?

  • Are you considering Salesforce for your company but unsure of where to begin?
  • Planning a Salesforce implementation and in need of seasoned expertise?
  • Already using Salesforce but not fully satisfied with the outcome?
  • Facing roadblocks in your Salesforce implementation and require assistance to progress?

Feel free to review my profile and reach out for tailored support to meet your needs!

Comments

About author

My photo
Jan Binder
Experienced Salesforce Technical Architect and Team Lead with a proven track record of delivering successful enterprise projects for major global companies across diverse industries, including automotive, oil & gas, and construction.