How to test a scheduler which runs a batch?

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();

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?"


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
}