2

I've written a batch class that also implements schedulable. I've written a unit test class and would like to be able to make additional assertions for the results of the batch. The problem is that I can't seem to find a way to determine when the class has completed its execution to hold off doing anything in the unit test long enough to query the actual results.

Below is the code from the schedulable batch classa and the test class I've written:

Schedulable Batch Class:

global class AccountContactRollupBatch extends BatchableTemplate {

    /**SCHEDULABLE TEMPLATE METHODS*/
    global override void schInit() {
        //Get the init vars from Custom Setting
        final AutoSchedule__c schJob = AutoSchedule__c.getValues('AutoSchedulable');

        super.setAutoSchedule(schJob.autoSchedule__c).
              setPeriodicity(schJob.periodicity__c.intValue()).
              setDeactivate(schJob.deactivate__c).
              setTerminate(schJob.terminate__c).
              setSendErrors(schJob.sendErrors__c).
              setEmailAddress(schJob.emailAddress__c).
              setFromBatchable(schJob.fromBatchable__c).
              setParallelExecutions(schJob.parallelExecutions__c);
    }

    /**BATCHABLE TEMPLATE METHODS*/
    global override Database.QueryLocator batchStart(Database.BatchableContext bc) {
        this.query = 'Select Id, Number_Of_Contacts__c, (Select Id From Contacts) From Account';

        return Database.getQueryLocator(query);
    }

    global override void batchExecute(Database.BatchableContext bc, List<sObject> scope) {
        list<Account>ToUp = new list<Account>();
        list<Contact>ActCtcs = new list<Contact>();
        integer count = 0;
        for(sObject s:scope){
            Account a = (Account)s;
            ActCtcs = a.Contacts;
            for(Contact ac:ActCtcs){
                Contact c = new Contact(id = ac.Id );
                count ++;               
            }
            a.Number_Of_Contacts__c = count;
            system.debug('acctid = ' + a.Id + ' CTC count = ' + a.Number_Of_Contacts__c );
            count = 0;
            ToUp.add(a);
        }   
        system.debug('ToUp size = ' + ToUp.size());         
        if(ToUp.isEmpty() == false) update ToUp;
    }

    global override void batchFinish(Database.BatchableContext bc) {

    }

}

Unit Test:

@isTest
public class TestBatchable {

public static testMethod void BatchableTest() {


   User testRunner = [Select Id FROM User WHERE Profile.Name = 'System Administrator' limit 1];

   system.runAs(testRunner){

// Create 200 test accounts - this simulates one execute.  
// Important - the Salesforce.com test framework only allows you to 
// test one execute.

If(AutoSchedule__c.getValues('AutoSchedulable') == null){

    AutoSchedule__c AutoSchedulable = new AutoSchedule__c();
    AutoSchedulable.Name = 'AutoSchedulable';
    AutoSchedulable.autoSchedule__c = false;
    AutoSchedulable.emailAddress__c = 'crmprogdev@gmail.com';
    AutoSchedulable.fromBatchable__c = false;   
    AutoSchedulable.periodicity__c = 1440;
    AutoSchedulable.sendErrors__c = false;
    AutoSchedulable.terminate__c = false;
    AutoSchedulable.deactivate__c = false;
    AutoSchedulable.parallelExecutions__c = false;

    upsert AutoSchedulable;
}


   List <Account> accns = new List<Account>();
      for(integer i = 0; i<200; i++){
         Account a = new Account(Name='testAccount'+'i', Ownerid = testRunner.Id ); 
         accns.add(a);
      }

    insert accns;

    // create 5 contacts for each account 

    list<Contact>Ctcs = new list<Contact>();

    for(Account a:accns ){
        integer x=0; 
        for(integer i=0; i<5; i++){

            contact c = new contact(accountId = a.Id, OwnerId = a.OwnerId, FirstName = 'Frst_'+ x + '_'+ i , LastName = 'Lst_'+ x + '_'+ i , email = 'tst'+x+'.'+i+'@testbatch.org');   

            Ctcs.add(c);
        }

    }

    insert Ctcs;



    final String minuteString = System.now().addMinutes(1).minute().format();
    Schedulable sch = new AccountContactRollupBatch();
    final String cron = '05 '+ minuteString + ' * * * ?';


    Test.StartTest();

    // Schedule the test job 

    string jobId = System.schedule('AccountContactRollupBatch', cron, sch);

    // Get the information from the CronTrigger API object
    CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime FROM CronTrigger WHERE id = :jobId];
    // Verify the expressions are the same
    System.assertEquals(cron, ct.CronExpression);

    // Verify the job has not run
    System.assertEquals(0, ct.TimesTriggered);

    // Verify when the job will run
    System.debug('next fire time = '+ ct.NextFireTime.date() + ', '+ ct.NextFireTime.hour() +', '+ ct.NextFireTime.minute());

    // Verify the scheduled job hasn't run on the data yet
    Account Tacct = [Select Id, Number_Of_Contacts__c FROM Account WHERE OwnerId = :testRunner.Id LIMIT 1];
    Boolean Result;
    try{ 
    Result = (Tacct.Number_Of_Contacts__c == null);
    //  system.assert(Result, true);
    }catch(nullPointerException e){
        system.debug('list exception because Number_of_Contacts__c isNull, Result = true'+ e);
        Result = true;
    }

    system.assert(Result, true);


   Test.StopTest();

    *** How does one know that a batch has completed? ***
    // query for completion
    // Verify the next time the job will run

    CronTrigger ct2 = [SELECT Id, CronExpression, TimesTriggered, NextFireTime, PreviousFireTime, EndTime, State FROM CronTrigger WHERE id = :jobId];

    if(ct2.TimesTriggered >0 && ct2.State == 'Completed'){
        System.debug('previous fire time = '+ ct2.NextFireTime.date() + ', End Time '+ ct2.EndTime +', State = '+ ct2.State + ' times triggered = ' + ct2.TimesTriggered );

        list<Account>AftrB = [SELECT Id,Number_Of_Contacts__c FROM Account];

        *** Line below seems to be the only reliable assertion that can be made ***
        system.assertEquals(AftrB.size(), 200);

        *** If job isn't complete, sorting through the records won't produce reliable results ***
        *** Debug logs always show the job completing, but can't determine when or delay the query ***
        for(Account a:AftrB){

        system.debug('acctId = '+ a.Id +' num of ctcs = '+ a.Number_Of_Contacts__c);

        // system.assertEquals(a.Number_Of_Contacts__c, 5);

        }

    }//end for loop

   }//end runAs
   }//end method
}//end class

You'll note in the final code code sections that follow Test.StopTest();, I'd like to be able to query the results of the batch job AFTER it's finished running. I can see the results in the debug log, but see no obvious way of delaying running the query for the results until the job has completed. Is there a mechanism for doing this?

The only thing I can think of is to give the unt test something "to do" that will "kill time", allowing the job time to complete before querying the results. I'll then be able to do assertions on the results of the batch operations. It seems there must be a better way to do this than creating "busy work tasks" to create idling time in a unit test, but I don't know what that is.

Can someone shed some light on how to deal with this issue?

Code for the abstract classes can be found at Valnavjo's Blog - Batchable Template and Valnavjo's Blog -Schedulable Template along with a more complete explanation of how it works. It uses custom settings to hold and change the intervals for the schedulable's intervals, termination of the schedulable, deactivation, etc.

2
  • one suggestion: could you remove your "WHERE id = :jobId" from the SOQL and see
    – Hasantha
    Oct 22, 2015 at 3:17
  • Yes, will try that. TY for the suggestion.
    – crmprogdev
    Oct 22, 2015 at 3:18

2 Answers 2

2

I think your SOQL for CronTrigger looking for State=COMPLETED jobs will never satisfy. What we could do is test the Batchable and schedulable functionality separately. After the StopTest what you can only check is a job with the state=WAITING

  1. test your logic in batchExecute method in a separate test method.
  2. test your schedule execution with checking the cronTrigger record with job id
1
  • What you're saying makes sense to me. In essence, I need to use the command line too start the batchable test rather than using the schedulable method. The one will test the schedulable methods, while the other will allow me to test the full results of the batchable method. I will try initiating a 2nd batch method using a command from the Abstracted Class to bypass the schedulable. Using the settings to turn the feature off doesn't appear to have been the way to have tested it.
    – crmprogdev
    Oct 22, 2015 at 11:57
0

When you call Test.stopTest(), the batch process runs automatically and synchronously. Immediately after Test.stopTest() returns, you can query your records and check any assertions you'd like.

A typical batchable unit test does something like:

System.executeBatch(someBatch);
Test.stopTest(); // Runs the batch
Account[] a = [SELECT Id, SomeValue__c FROM Account];
// Do some validations
5
  • Yes, I know it runs immediately, but it takes some time to complete. If I query immediately, the results that are returned aren't on the records that have been committed.
    – crmprogdev
    Oct 22, 2015 at 3:12
  • @crmprogdev You'd be writing your code wrong, somehow. Trust me, when you get to the line after Test.stopTest(), any results from the batch are already available to unit test. Perhaps you've got an exception somehow?
    – sfdcfox
    Oct 22, 2015 at 3:14
  • No exceptions. It all runs fine. It's a case of what's returned in the query doesn't show the correct results for the updated records if it's run immediately. The debug logs show the records were updated, but the query results don't.
    – crmprogdev
    Oct 22, 2015 at 3:16
  • @crmprogdev Hmm. I think we need a closer look at your code. I've done this hundreds of times, it works as advertised. There's got to be something you've overlooked.
    – sfdcfox
    Oct 22, 2015 at 3:19
  • Sure. Will edit my post with more code. This is a "test" of a schedulable batch method using a simple scenario that's trivial before using it for other purposes. This is built from abstract classes that I'll provide the links to rather than posting tons of extra code unless you'd like to see it all in one place.
    – crmprogdev
    Oct 22, 2015 at 3:37

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .