9

I have a scheduler class which calls a batch Apex. I want to write an Apex test for the scheduler class. However, the test fails. It seems that the testing framework does not execute the batch class.

I'm providing below a complete example that illustrates the problem. I wrote two test methods. The first one calls directly the batch, and this test method is successful. The second test method uses the scheduler instead of the batch. Since the scheduler class calls the batch, I'm expecting the same result than the first test method. However, the assertion fails in the second test method.

@isTest
public class AccountBachAndSchedulerTestSuite {
    static Integer numberOfAccounts {get; set;}
    static {
        numberOfAccounts = 50;
    }

    // Working test: we insert accounts, then we call the batch which sets
    // the Is_Checked__c field to true for all accounts
    @isTest static void testBatchOnly () {
        List<Account> lst_accounts = new List<Account>();
        for(Integer i = 0; i < numberOfAccounts; i++) {
            lst_accounts.add(New Account(
                Name = 'Test Account' + i,
                Is_Checked__c = false
            ));
        }
        insert lst_accounts;

        // call the batch
        Test.startTest();
        ID BatchProcessdId = Database.executeBatch(new AccountBatch());
        Test.stopTest();

        // Assert result: Is_Checked__c field must be set to true for all accounts
        Integer expectedNumOfAccounts = [SELECT COUNT() FROM ACCOUNT WHERE Is_Checked__c = true];
        System.assertEquals(numberOfAccounts, expectedNumOfAccounts);   // Assertion OK
    }

    // Failing test: same test than the first one. Only this time we call the
    // scheduler class instead of the batch
    @isTest static void testScheduler () {
        // CRON expression: midnight on March 15. Because this is a test, 
        // job is supposed to execute immediately after Test.stopTest()
        String cronExpr = '0 0 0 15 3 ? 2022';
        String jobRunningTime = '2022-03-15 00:00:00';

        List<Account> lst_accounts = new List<Account>();
        for(Integer i = 0; i < numberOfAccounts; i++) {
            lst_accounts.add(New Account(
                Name = 'Test Account' + i,
                Is_Checked__c = false
            ));
        }
        insert lst_accounts;

        Test.startTest();
        // Schedule the test job
        String jobId = 
            System.schedule('myJobTestJobName', cronExpr, new AccountBatchScheduler());
        Test.stopTest();

        // Assert result: Is_Checked__c field must be set to true for all accounts
        Integer actualNumOfAccounts = [SELECT COUNT() FROM ACCOUNT WHERE Is_Checked__c = true];
        System.assertEquals(numberOfAccounts, actualNumOfAccounts); // Asertion FAILED: 
        // Expected numberOfAccounts (=50), but actual is zero.
    }
}

Here is the Scheduler class:

global class AccountBatchScheduler implements Schedulable {
    global void execute(SchedulableContext sc) {
        ID lv_BatchProcessdId = Database.executeBatch(new AccountBatch());
    }
}

Here is the batch class:

// This batch sets the field Is_Checked__c to true for all accounts
global class AccountBatch implements Database.Batchable<sObject>, Database.Stateful {
    global final String myQuery = 'SELECT Id, Is_Checked__c FROM Account';
    global String errMsg = '';

    global Database.QueryLocator start(Database.BatchableContext BC) {
        return Database.getQueryLocator(myQuery);
    }

    global void execute(Database.BatchableContext BC, List<sObject> scope) {
        System.debug('Batch [AccountBatch] execute');
        List<Account> lst_accounts = (List<Account>) scope;
        for(Account acc : lst_accounts) {
            acc.Is_Checked__c = true;
        }
        // Update all Accounts
        if (!lst_accounts.isEmpty()) {
            Database.saveResult[] lst_updatetResults = Database.update(lst_accounts, false);
            for (Database.SaveResult singleResult : lst_updatetResults) {
                if (!singleResult.isSuccess()) {
                    errMsg += 'Batch [AccountBatch] Account Update error. ErrMsg = ' + 
                        singleResult.getErrors()[0].getMessage() + '<br />';
                }
            }
        }   
    }

    global void finish(Database.BatchableContext BC) {
        if (errMsg == '') {
            System.debug('Batch [AccountBatch] has finished : All Account updates are successful!');
        }
        else {
            System.debug('Batch [AccountBatch] has finished : There were errors. Here are a list of all errors. <br />' + errMsg);
        }
    }
}

Thank you for your help!!

13

I had a similar problem before which I solved by explicitly calling the execute method of my Schedulable class.

Try something like this:

Test.startTest();
AccountBatchScheduler abs= new AccountBatchScheduler();
String jobId = System.schedule('myJobTestJobName', cronExpr, abs);
abs.execute(null);
Test.stopTest();
  • 1
    I would suspect this will cause the batch class to execute twice: once when you call Test.stopTest() and once when you call abs.execute(). – Swisher Sweet Apr 26 '17 at 20:15
  • 2
    @SwisherSweet It's been a while, but I'm pretty sure that I added the explicit execute() call because the Test.stopTest() didn't execute the scheduler, as it would be expected. – smukov Apr 26 '17 at 20:22
  • 4
    Confirm (API v40): a batch job scheduled with System.schedule is not executed unless the explicit execute() call in a test class. – edelrabe Oct 16 '17 at 14:10
4

It's not going to run the batch. Since you have already tested the batch actions separately, you just need to query for the CronTrigger and verify it got scheduled. That's all that Test.stopTest() will guarantee, execution-wise. Jobs will be scheduled, but not executed.

Think about it this way, your stopTest call guarantees that all asynchronous processes will fire, but that only tells you that your executeBatch call will get hit. Since that functionality is also asynchronous, you would need to be able to call stopTest a second time to ensure the batch will execute.

static testMethod void testSchedule()
{
    // setup data

    Test.startTest();
        insert data;
    Test.stopTest();

    system.assertEquals(1, [SELECT count() FROM CronTrigger],
        'A job should be scheduled');
}
static testMethod void testBatch()
{
    // setup data
    insert data;

    Test.startTest();
        Database.executeBatch(new MyBatch());
    Test.stopTest();

    // assert on batch behavior
}
  • "Jobs will be scheduled, but not executed." - I have found this to not be true. Calling Test.stopTest in a schedule test class will execute the batch class. – Swisher Sweet Apr 26 '17 at 20:12
  • 2
    @SwisherSweet It will cause the batch to execute if you called Database.executeBatch beforehand. But not if you called system.scheduleJob. – Adrian Larson Apr 26 '17 at 21:06
4

Thanks for posting the question, this is a weird one.

As the Salesforce documention says, Test.stopTest() should force asynchronous processes to run. It works for Apex Batch on its own, and it works for Schedulable on its own. But when you have a Schedulable that runs an Apex Batch, it doesn't behave as expected: It looks like Test.stopTest() does force the Schedulable execute() method to get called. But the inner Apex Batch does not get executed.

Perhaps this is a bug.

However I think we can approach this situation a bit differently: What do we actually want to test here?

The Salesforce examples for testing a Schedulable have lots of messing about with CronTrigger to verify that the scheduling has happened. But that seems a waste of time to me, because you're just testing core Salesforce behaviour rather than the behvaiour of your own code.

The actual functionality of your Apex Batch is (presumably) tested somewhere else already.

So what we really want to test is that:

  • The Scheduler execute() behaves as expected
  • i.e. The Apex Batch gets posted to the queue

We dont actually want to test that the Apex Batch executes - that should be covered elsewhere.

You can test these things by querying the AsyncApexJob queue. In a test environment AsyncApexJob will be empty unless you schedule something.

So you can write your test like this:

@isTest static void testScheduler () {
    // CRON expression: midnight on March 15. Because this is a test, 
    // job is supposed to execute immediately after Test.stopTest()
    String cronExpr = '0 0 0 15 3 ? 2022';

    // NB: test data not needed, because we're not testing Apex Batch results
    // That will be tested somewhere else

    // Verify that AsyncApexJob is empty
    // not strictly necessary but makes what is going on later clearer
    List<AsyncApexJob> jobsBefore = [select Id, ApexClassID, ApexClass.Name, Status, JobType from AsyncApexJob];
    System.assertEquals(0, jobsBefore.size(), 'not expecting any asyncjobs');

    Test.startTest();
    // Schedule the test job
    String jobId = 
        System.schedule('myJobTestJobName', cronExpr, new AccountBatchScheduler());
    Test.stopTest();

    // There will now be two things in AsyncApexJob - the Schedulable itself
    // and also the Batch Apex job. This code looks for both of them

    // Check schedulable is in the job list
    List<AsyncApexJob> jobsScheduled = [select Id, ApexClassID, ApexClass.Name, Status, JobType from AsyncApexJob where JobType = 'ScheduledApex'];
    System.assertEquals(1, jobsScheduled.size(), 'expecting one scheduled job');
    System.assertEquals('AccountBatchScheduler', jobsScheduled[0].ApexClass.Name, 'expecting specific scheduled job');

    // check apex batch is in the job list
    List<AsyncApexJob> jobsApexBatch = [select Id, ApexClassID, ApexClass.Name, Status, JobType from AsyncApexJob where JobType = 'BatchApex'];
    System.assertEquals(1, jobsApexBatch.size(), 'expecting one apex batch job');
    System.assertEquals('AccountBatch', jobsApexBatch[0].ApexClass.Name, 'expecting specific batch job');


}

I think a test like this works around the slightly weird Salesforce behaviour here, and also, arguably, is properly testing the thing that actually needs to be tested. Which is: "Did my Apex Batch get posted to the queue?"

0

Recently had the same problem: when testing the scheduler that run batch - batch wasn't executed. Found the solution here: Does Test.stopTest() ensure a System.Schedule/Database.Batchable completes in a test?

If you need to run batch in scheduler test, set the scheduler in @testsetup method and surround it with Test.start() Test.stop(); Then in any test method verify that batch was run.

But to my mind for scheduler test it's enough to check that scheduler scheduled the job. And for batch to wright a separate test. Otherwise scheduler and batch tests would check pretty the same things.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.