function readOnly(count){ }
Starting November 20, the site will be set to read-only. On December 4, 2023,
forum discussions will move to the Trailblazer Community.
+ Start a Discussion
jkucera3jkucera3 

"No more than one executeBatch can be called from within a test method"

Nothing changed with my test - it only calls one batch job.  This worked 2wks ago, but now is failing, preventing me from uploading an important bug fix to my customers.

 

22:56:34.835|EXCEPTION_THROWN|[EXTERNAL]|System.UnexpectedException: No more than one executeBatch can be called from within a testmethod. Please make sure the iterable returned from your start method matches the batch size, resulting in one executeBatch invocation.

 

My only change was an addition of a 3rd test that calls the same method "TryBatchJobsAgain", which passes, despite this one failing.  Perhaps the test doesn't truly finish the batch job before the other test starts?

 

When I comment out this test, an unrelated test fails that calls a separate batch job.

 

When I move that test to a completely separate test class, it still fails.  

 

Test Method:

 

public static testMethod void testRemoveDelayBatchJobFromQueue() {
        Long numRecordsToCreate=2;
        Long numUsersToCreate=2;
        String profileName='System Administrator';

        Boolean Active=True;
        Double URDaysDelay=2;
        String URObjectName='UnfollowTest__c';
        String URFieldName='String__c';
        String UROperator='equals';
        String URValue='a';//testing capitalization as well
        List<UnfollowRule__c> urs=new List<UnfollowRule__c>();


        String Str='a';
        Boolean Check=TRUE;
        String Pick='2';
        Double Dec=1000;
        Date Dat=date.today();
        DateTime DatTim=dateTime.now();
        String Phone = '415-555-5555';
        String Email = 'test@test.com';
        String Url='www.test.com';
          
        cleanUpTestData();
        
        urs.add(createUR(Active, URObjectName, URFieldName, UROperator, URValue, URDaysDelay ));    
        insert urs;
        
        List<Id> recordIds=createUnfollowTestRecords(numRecordsToCreate,Check, Dat, DatTim, Dec, Email, Phone, Pick, Str, Url);
        List<User> users=createUsers(numUsersToCreate, profileName);
        List<EntitySubscription> subs=createSubs(users,recordIds);
        List<UnfollowQueue__c> uqs=new List<UnfollowQueue__c>();
        for (Id i:recordIds){
            UnfollowQueue__c uq=new UnfollowQueue__c();
            uq.recordId__c=i;
            uq.CriteriaMetDate__c=Date.Today()-URDaysDelay.intValue();
            uq.DaysDelay__c=URDaysDelay;
            uqs.add(uq);
        }//for 1

        List<Id> recordIds2=createUnfollowTestRecords(numRecordsToCreate,Check, Dat, DatTim, Dec, Email, Phone, Pick, Str, Url);
        List<EntitySubscription> subs2=createSubs(users,recordIds2);

        for (Id i:recordIds2){
            UnfollowQueue__c uq=new UnfollowQueue__c();
            uq.recordId__c=i;
            uq.CriteriaMetDate__c=Date.Today()-URDaysDelay.intValue()+1;
            uq.DaysDelay__c=URDaysDelay;
            uqs.add(uq);
        }//for 1

        insert uqs;
        
        Boolean delayJob = TRUE;
        Boolean delayRulesIncluded=FALSE;
        Boolean evalateEachRecordForDaysDelay=FALSE;
        Boolean addFieldNames=TRUE;
        String sObjectQuery = 'Select Id, chttrunfollow__recordId__c FROM chttrunfollow__UnfollowQueue__c WHERE chttrunfollow__scheduledUnfollowDate__c<= TODAY AND IsDeleted=FALSE';
        UnfollowBatchJobsQueue__c job=new UnfollowBatchJobsQueue__c(delayJob__c=delayJob, delayRulesIncluded__c=delayRulesIncluded, evalateEachRecordForDaysDelay__c=evalateEachRecordForDaysDelay, objectName__c=URobjectName, numRulesUsedInThisObject__c=urs.size(), sObjectQuery__c=sObjectQuery);
        insert job;
        
        system.debug('Unfollow Queue Size: '+[SELECT Id FROM UnfollowBatchJobsQueue__c].size());
        
        test.StartTest();
            unfollowTryBatchJobsAgain.unfollowTryBatchJobsAgain();//This is a regular class that calls 1 batch job        test.stopTest();
        
        //first confirm the job was removed from the queue
        List<UnfollowBatchJobsQueue__c> bjQueue = [SELECT Id FROM UnfollowBatchJobsQueue__c];
        system.AssertEquals(0,bjQueue.size());
        //then confirm it actually worked
        List<EntitySubscription> es=[Select Id FROM EntitySubscription WHERE ParentId IN :recordIds];
        system.assertEquals(0,es.size());
        uqs=[SELECT ID FROM UnfollowQueue__c WHERE ScheduledUnfollowDate__c<=TODAY];
        system.assertEquals(0,uqs.size());
        es=[Select Id FROM EntitySubscription WHERE ParentId IN:recordIds2];
        system.assertEquals(numRecordsToCreate*numUsersToCreate,es.size());
    }//testRemoveDelayBatchJobFromQueue

 

 

Note the highlighted red below is the ONLY batch job executed from this test - even the system logs confirm this.

 

global with sharing class unfollowTryBatchJobsAgain{

    public static void unfollowTryBatchJobsAgain(){
        Integer numBatchApexJobsLimit=5;//at time of coding, there are at most 5 concurrent batch apex jobs in any org
        List<AsyncApexJob> numBatchJobs = [SELECT Id, Status FROM AsyncApexJob WHERE Status = 'Queued' OR Status = 'Processing'];

        //This is the number of jobs that can be queued up by this method
        Integer numJobsAvailable=numBatchApexJobsLimit - numBatchJobs.size();
	
        if(numJobsAvailable>0){
            List<UnfollowBatchJobsQueue__c> batchJobsQueued=[SELECT Id, IsDeleted, delayJob__c, delayRulesIncluded__c, evalateEachRecordForDaysDelay__c, numRulesUsedInThisObject__c, objectName__c, sObjectQuery__c FROM UnfollowBatchJobsQueue__c WHERE IsDeleted=FALSE ORDER BY  CreatedDate ASC];
            //Goal here is to process the delay queue first as it's more important than the others. Rather than do 2 queries, it's handled with variables here:
            Integer delayJobNum=1000;//initialize to huge number as a backup
            for (Integer i=0;i<batchJobsQueued.size();i++){
                if (batchJobsQueued[i].delayJob__c==TRUE){
                    delayJobNum=i;
                    break;
                }//if 2
            }//for 1
    		List<UnfollowBatchJobsQueue__c> batchJobsToDelete=new List<UnfollowBatchJobsQueue__c>();        
            for(Integer i=0; i<numJobsAvailable && i<batchJobsQueued.size(); i++){
                //if this is the high priority "delayed records scheduled for unfollow today" job, do it first
                if (delayJobNum!=1000){
                    UnfollowProcessUnfollowQueueBatch unfollowDelayedRecords= new UnfollowProcessUnfollowQueueBatch();
                    unfollowDelayedRecords.sObjectQuery=batchJobsQueued[delayJobNum].sObjectQuery__c;                    try{
                        Id unfollowRulesProcessId = Database.executeBatch(unfollowDelayedRecords, 200); 
                        batchJobsToDelete.add( batchJobsQueued[delayJobNum]);
                    } catch(exception e){
//                        system.debug('Either the batch failed or the job deletion from teh queue failed: '+e);
                    }//try
                } else if(batchJobsQueued[i].delayRulesIncluded__c==FALSE){
                 //is this the simple case with no "days delay" rules?
                    UnfollowRecordsBatch  unfollowRecords= new UnfollowRecordsBatch();
                    unfollowRecords.ObjectName=batchJobsQueued[i].objectName__c;
                    unfollowRecords.numRulesUsedInThisObject=batchJobsQueued[i].numRulesUsedInThisObject__c.intValue();
                    unfollowRecords.sObjectQuery =  batchJobsQueued[i].sObjectQuery__c;
                
                    try{
                        Id unfollowRulesProcessId = Database.executeBatch(unfollowRecords, 200); 
                        batchJobsToDelete.add(batchJobsQueued[i]);
                    } catch(exception e){
//                        system.debug('Either the batch failed or the job deletion from the queue failed: '+e);
                    }//try
                } else {
                //else it's the more complex case where we need to check for the unfollow date
                    UnfollowQueueDelayRecordsBatch queueDelayRecords= new UnfollowQueueDelayRecordsBatch();
                    queueDelayRecords.ObjectName=batchJobsQueued[i].objectName__c;
                    queueDelayRecords.sObjectQuery =  batchJobsQueued[i].sObjectQuery__c;
                    queueDelayRecords.evalateEachRecordForDaysDelay=batchJobsQueued[i].evalateEachRecordForDaysDelay__c;
                    
//let's cross our fingers that the rule criteria didn't change between when this job first ran and now :(  
//Will the code fail elegantly if the rules were changed?
//I'd rather not create a 3rd queue just to save the state of the rules due to stupid batch apex limits
                    queueDelayRecords.delayRules=[Select Id, ObjectName__c, Active__c, FieldName__c, FieldType__c, Operator__c, Value__c, DaysDelay__c FROM UnfollowRule__c WHERE DaysDelay__c>0 AND Active__c = TRUE AND objectName__c=:queueDelayRecords.ObjectName]; 

                    try{
                        Id unfollowRulesProcessId = Database.executeBatch(queueDelayRecords, 200); 
                        batchJobsToDelete.add( batchJobsQueued[i]);
                    } catch(exception e){
//                        system.debug('Either the batch failed or the job deletion from the queue failed: '+e);
                    }//try
                    
                }//if 2
            }//for 1
            try{
            	delete batchJobsToDelete;
            } catch(exception e){
//                        system.debug('job deletion from the queue failed: '+e);            
            }//try delete

        }//if 1


    }//unfollowTryBatchJobsAgain
}//unfollowTryBatchJobsAgain

 

:

 

 

 

 

 

Best Answer chosen by Admin (Salesforce Developers) 
jkucera3jkucera3

Thanks for the reply - it ended up being an issue with existing data in my org.  I use my org to test the app, and have it regularly process records.  The problem is that I didn't delete them all before running the tests, so there were more than 200 records found in the query, which Apex threw up on.  I'm still not a big fan of Apex using the org's actual data in queries instead of only test data, but that's for another thread.

 

Deleting the records and bringing it below 200 meant 1 batch, which eliminated the error.

 

I did add the abort job code for cleanliness as well in case that pops up down the road.

All Answers

CLKCLK

try using System.abortJob(batchID) at the end of every test method for batch apex.

jkucera3jkucera3

Thanks for the reply - it ended up being an issue with existing data in my org.  I use my org to test the app, and have it regularly process records.  The problem is that I didn't delete them all before running the tests, so there were more than 200 records found in the query, which Apex threw up on.  I'm still not a big fan of Apex using the org's actual data in queries instead of only test data, but that's for another thread.

 

Deleting the records and bringing it below 200 meant 1 batch, which eliminated the error.

 

I did add the abort job code for cleanliness as well in case that pops up down the road.

This was selected as the best answer
sfdcfoxsfdcfox
Each test method gets it's own governor limits (such as an executeBatch), so you don't need to abortJob.

I just thought I'd add here that you might drop the SeeAllData=true annotation and/or move your test code to at least API 25.0 so as to take advantage of the data isolation feature, if possible.

I know that the isolation feature has some glitches to work out (such as you can't access the Standard Price Book), but using data isolation would eliminate this type of org data dependency. This might make a difference later when your test has to delete more than 50k records just to execute itself.

As a final alternative solution, you might update the utility class to see if Test.isRunningTest() is true, and if so, call the executeBatch function with a parameter to limit the call to 200 records.
RajeevJainRajeevJain

Workarounds : 
=============

>> Try to pass the less than 200 records in test class.

>> What you have to consider is using the Test.isRunningTest to bypass the code starting the second job in this context. Meaning that you will have to test your second batch job in a separate test to get coverage and assert behaviour. In doing so you will obviously have to reproduce manually in the test code the state in the database the second job expects. Not ideal, but should work.

 
The following is an example of the change to avoid the second batch job being executed during test execution.
 
 
public void finish(Database.BatchableContext)
{
    if(!Test.isRunningTest)
         Database.executeBatch(new MySecondBatchJob));
}


>> Enclose the executeBatch in Test.startTest() and Test.stopTest()

>> always try to use @SeeAllData=false
Ramakrishna Madha 6Ramakrishna Madha 6
Error: "No more than one executeBatch can be called from within a test method"

Reaon: Your batch class dealing with more than 200 records. It means a single batch can process only 200 records, if there are more than 200 records exisits then your job will be executed in multiple batches of 200 records each. It is ok for dealing with actual processing. But in test classes, it will deal with only one batch class which is nothing but upto 200 records only.

Solution:
Use Test.isRunningTest() condition in your main code/class to bypass this and test it for only upto 200 records which is nothing but one batch will be exuted this is what error suggesting. Then terminate the job to stop executing remaing records.

If(Test.isRunningTest()){
      ID jobID = Database.executeBatch(new BatchClassName, 200);
      System.abortJob(jobID);
}else{
      Database.executeBatch(new BatchClassName);
}

Happy coding
Kris MarianoKris Mariano
Hi,

I have also encountered the same problem, and what I did to solve this is I had another query for testing, and I limited it to 200.
 
if(Test.isRunningTest()){
                query = 'SELECT Id, Status__c, OwnerId, EndDateTime, Owner.UserRole.Name FROM Event WHERE Status__c != \''+missedStatus+'\' LIMIT 200';
            }else{
                query = 'SELECT Id, Status__c, OwnerId, EndDateTime, Owner.UserRole.Name FROM Event WHERE CreatedDate = Yesterday AND Status__c != \''+missedStatus+'\'';
            }

Hope this answers the problem who might encounter the same problem in the future.
Rohith Raj Chenna TEIRohith Raj Chenna TEI
This worked Kris. Thank you!