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
Lee SinLee Sin 

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.

/*
 * **Created by Jack Wang 9.24.2014
*/
global class AR_Creating75DummyNML implements Database.Batchable<sObject>{
    
    static final Integer numofNML=75;
    global final String query;
    global AR_Creating75DummyNML(String q)
    {
        query=q;
    }
    
    public List<Task> createNML(List<ID> IDs)
    {
        integer size=IDs.size();
        List<integer> ints=new List<integer>();
        
        Integer numberDays = date.daysInMonth(Date.today().year(), Date.today().month());
        for(integer i=0;i<75;i++)
        {
            double k=Math.random()*size;
            ints.add(k.intValue());          
        }
        
        
        string userid=null;
        User[] users=[select id from User where name='Jeremy Young'];
        if(users.size()>0)
        {
            userid=users[0].id;
        }
        List<Task> tsks=new List<Task>();
        for(Integer i : ints)
        {
            double datek=Math.random()*numberDays;
            Task nml=new Task(ownerid=userid,CallType__c='NML',whatid=String.valueOf(IDs[i]),Subject='NML',
                              Status='Completed',Priority='Normal',ActivityDate=Date.today().tostartofMonth().addDays(datek.intValue()));
            tsks.add(nml);
        }
        return tsks;
    }
    
    global Database.QueryLocator start(Database.BatchableContext BC)
    {        
        return Database.getQueryLocator(query);        
    }
    
    global void execute(Database.BatchableContext BC, List<Account> scope)
    {
        
        Map<String,List<ID>> territoryClientIDMap=new Map<String,List<ID>>();
        for(Account client : scope)
        {
            if(territoryClientIDMap.containsKey(client.client_territories__c))
            {
                territoryClientIDMap.get(client.client_territories__c).add(client.Id);
            }
            else
            {
                territoryClientIDMap.put(client.client_territories__c, new List<ID>());
                territoryClientIDMap.get(client.client_territories__c).add(client.Id);
            }
        }
        
        for(List<ID> clientIDS : territoryClientIDMap.values())
        {
            List<Task> tsks=createNML(clientIDS);
            insert tsks;
        }
        
        
    }
    
    global void finish(Database.BatchableContext BC)
    {
        
    }

}
@isTest
private class Test_AR_Creating75DummyNML {
    
    
    static testMethod void Test(){
        //create 10 advisors
        ////for each advisor create 100 clients
        //

        string userid=null;
        User[] users=[select id from User where name='Jeremy Young'];
        if(users.size()>0)
        {
            userid=users[0].id;
        }
        List<Account> advisors=new List<Account>();
    
        For(integer i=0;i<10;i++)
        {
            Account a=new Account(name='a'+String.valueOf(i),client_territories__c='a'+String.valueOf(i),account_type__c='provider',Monthly_Activity_Reporting__c=true,Account_Status__c='active');
            advisors.add(a);
        }
        insert advisors;
        
        For(integer j=0;j<advisors.size();j++)
        {
            List<Account> clients=new List<Account>();
            For(integer i=0;i<100;i++)
            {
                Account client=new Account(name=advisors[j].name+String.valueOf(i),client_territories__c=advisors[j].client_territories__c,account_type__c='prospect');
                clients.add(client);
            }
            insert clients;
        }
        
        
        List<Account> advisors1= [select client_territories__c from Account where
                                (account_type__c='provider' or account_type__c='advisor') 
                                 and Monthly_Activity_Reporting__c=true and Account_Status__c='active'];
        List<String> territories=new List<String>();
        List<String> advisorIDs=new List<String>();
        for(Account advisor : advisors1)
        {
            territories.add(advisor.client_territories__c);
            advisorIDs.add(advisor.id);
        }        
        //[select id,client_territories__c from Account where client_territories__c in :territories];
        string tempquery='Select id,client_territories__c from Account where client_territories__c in (';        
        for(String terr : territories)
        {
            tempquery+='\'';
            tempquery+=terr;
            tempquery+='\',';
        }
        tempquery=tempquery.substring(0, tempquery.length()-1);
        tempquery+=') and id not in (';
        for(String adid : advisorIDs)
        {
			tempquery+='\'';
            tempquery+=adid;
            tempquery+='\'';
            tempquery+=',';
        }
        string query=tempquery.substring(0, tempquery.length()-1);
        query+=')';
        
        Test.startTest();
        AR_Creating75DummyNML c=new AR_Creating75DummyNML(query);
        Database.executeBatch(c);
        Test.stopTest();
        
        List<Task> tsk=[select ownerid,CallType__c,Subject,
                              Status,Priority,ActivityDate from Task where createddate=TODAY and CallType__c='NML'
                       and ownerid= :userid and Status='Completed' and Priority='Normal'];
        
        //75 tasks for each advisor
        System.assertEquals(750, tsk.size());
        
        
	}

}

I tried to google this error, but I didn't get a satisfied answer.


Best Answer chosen by Lee Sin
Deepak Kumar ShyoranDeepak Kumar Shyoran
Have you tried to use System.abortJob(batchID) after the execution of one batch if not then please try this it will help you to execute another batch or use Another Test.StartTest() and Test.stopTest() to execute another batch.

All Answers

Deepak Kumar ShyoranDeepak Kumar Shyoran
Have you tried to use System.abortJob(batchID) after the execution of one batch if not then please try this it will help you to execute another batch or use Another Test.StartTest() and Test.stopTest() to execute another batch.
This was selected as the best answer
Lee SinLee Sin
Deepak,
Do you mean :
Test.startTest();
        AR_Creating75DummyNML c=new AR_Creating75DummyNML(query);
        ID batchID=Database.executeBatch(c);
        System.abortJob(batchID);
        Test.stopTest();


Deepak Kumar ShyoranDeepak Kumar Shyoran
Yes this is my point of concern. Is it working for you ?
SForceBeWithYouSForceBeWithYou
Test.startTest();
Id batchJobId = Database.executeBatch(batchInstance, 1);
System.abort(batchJobId);
Test.stopTest();

While this does remove the error about System.UnexpectedException error, my batch ran the constructor and start method, but never entered the execute method, meaning all of that code was not covered, and my assertions failed after the Test.stopTest();

What DID work for me was to call Database.executeBatch with a scope large enough so that only one execute() method call runs during the test context.  You may have to adjust some of your batch logic if it is meant to be called with a small number of records and you have more records returned from your start query that can fit in one batch execution context.

For me, that meant:
Id batchJobId = Database.executeBatch(batchInstance, 2);
Since I had created two case records for my unit test, and the query ran through all cases.

Hope that helps!

Nathan
 
Kelly Logan (Raptek)Kelly Logan (Raptek)
I am seeing this error, but only when validating a change set uploaded to production. In the sandbox, the test code runs without issue. One difference is that the production system has around 8k of entries to process, while the sandbox has barely a hundred. I tried adding a batch size setting of 10k to cover that, but it didn't seem to make a difference. Also this is based on similar code that had no problem in production previously so it does lead me to wonder if one of the recent releases may be affecting this.

Here is the essential class that is being tested. Application__c is a custom object that has a Master-detail relationship with Account (or schools). The goal is to grant users/employees of the school Accounts permissions to Contacts that have Applications connected to those school Accounts. So College counselors can see the student data for applicants.
global class AppPermissionBatchService implements Schedulable, Database.Batchable<sObject>, Database.Stateful {
    // This class runs the process of creating Application-based permissions, to give College Partners the permission
    //  to see students for their colleges based on applications rather than student terms. This allows partners to
    //   see students with applications for future attendanced.
	global Map<String, Group> groupMap;
	global Map<String, Map<Id,CommunitiesUserPermissionService.ContactShareMatch>> existingGroupSharingRulesMap;
	global List<ContactShare> csList;

	global static String scheduleBatchJob(String cron) {
    AppPermissionBatchService SC = new AppPermissionBatchService();
    String cron_exp = cron != null ? cron : '0 0 0/1 1/1 * ? *';
    return System.schedule('Application based Permissions Batch Job', cron_exp, SC);
  }

  global static void executeBatchOnDemand(Integer batchSize) {
    AppPermissionBatchService batch = new AppPermissionBatchService();
  	Id batchprocessId = Database.executeBatch(batch, batchSize);
  }

	global void execute(SchedulableContext sc) {
    Id batchprocessId = Database.executeBatch(this, 1000);
  }

	global AppPermissionBatchService() {

	}

	global Database.QueryLocator start(Database.BatchableContext BC) {
		CommunitiesUserPermissionService.userPermissionsForCommunity();
		groupMap = CommunitiesUserPermissionService.getUniversityGroups();
		existingGroupSharingRulesMap = CommunitiesUserPermissionService.getExistingGroupSharingRules(groupMap);
		csList = new List<ContactShare>();
		return Database.getQueryLocator([Select Id, student_name__c, School_Name__c, Mid_Year_Transfer_School__c,
                                         Application_Submission_Year__c,
                                         School_Name__r.recordtype.name,
                                         Mid_Year_Transfer_School__r.recordtype.name
                                       	From Application__c
                                        Where LastModifiedDate = LAST_N_DAYS:3 AND
                                         ((School_Name__c != null AND 
                                               School_Name__r.recordtype.name LIKE 'College/University') OR
                                         	(Mid_Year_Transfer_School__c != null AND 
                                               Mid_Year_Transfer_School__r.recordtype.name LIKE 'College/University'))
                                        ]);
	}

    global void checkCollege(ID college,ID studentID){
        CommunitiesUserPermissionService.ContactShareMatch existingContactSharingRule = null;
        if(existingGroupSharingRulesMap.get(groupMap.get('grp_' + college).Id) != null) {
            existingContactSharingRule = existingGroupSharingRulesMap.get(groupMap.get('grp_' + college).Id).get(studentID);
        }
        
        if (existingContactSharingRule == null) {
            system.debug('making sharing rule');
            ContactShare cs = new ContactShare();
            cs.contactAccessLevel = 'Edit';
            cs.userOrGroupId = groupMap.get('grp_' + college).Id;
            cs.contactId = studentID;
            csList.add(cs);
            CommunitiesUserPermissionService.ContactShareMatch csm = new CommunitiesUserPermissionService.ContactShareMatch();
            csm.matchFound = true;
            csm.rule = cs;
            if(existingGroupSharingRulesMap.get(groupMap.get('grp_' + college).Id) == null) {
                existingGroupSharingRulesMap.put(groupMap.get('grp_' + college).Id, new Map<Id,CommunitiesUserPermissionService.ContactShareMatch>());
            }
            existingGroupSharingRulesMap.get(groupMap.get('grp_' + college).Id).put(studentID, csm);
        } else {
            existingContactSharingRule.matchFound = true;
            System.debug('Existing Rule ' + existingContactSharingRule);
        }
    }
         
	global void execute(Database.BatchableContext BC, List<Application__c> scope) {
		for (Application__c ap : scope) {
            // check school_name and Mid_Year_transfer, each calling the same method.
            // 
            System.debug('School Name: '+ap.School_Name__c+', groupMap get result: '+groupMap.get('grp_' + ap.School_Name__c));
			if (ap.School_Name__c != null && groupMap.get('grp_' + ap.School_Name__c) != null) {
				checkCollege(ap.School_Name__c,ap.student_name__c);
			}
            System.debug('School Name: '+ap.Mid_Year_Transfer_School__c+', groupMap get result: '+
                         groupMap.get('grp_' + ap.Mid_Year_Transfer_School__c));
			if (ap.Mid_Year_Transfer_School__c != null && groupMap.get('grp_' + ap.Mid_Year_Transfer_School__c) != null) {
				checkCollege(ap.Mid_Year_Transfer_School__c,ap.student_name__c);
			}
		}
	}

	global void finish(Database.BatchableContext BC) {
		System.debug(csList);
		if (! csList.isEmpty()) insert csList;
	}

}
Note that it and the earlier version of Student Term based permissions both used a Community service class that creates groups:
public with sharing class CommunitiesUserPermissionService {

  public static Map<String, Group> getUniversityGroups() {
    Map<String, Group> groupMap = new Map<String, Group>();
    for (Group g : [SELECT Id, developerName,name FROM Group WHERE type = 'Regular' AND developerName LIKE 'grp_%']) {
      groupMap.put(g.developerName, g);
    }
    return groupMap;
  }

  public class ContactShareMatch {
    public Id contactShareId {get; set;}
    public ContactShare rule {get; set;}
    public Boolean matchFound {get; set;}
  }

  public static Map<String, Map<Id, ContactShareMatch>> getExistingGroupSharingRules(Map<String, Group> groupMap) {
    Map<String, Map<Id, ContactShareMatch>> existingGroupSharingRulesMap = new Map<String, Map<Id, ContactShareMatch>>();
    for (ContactShare cs : [Select Id, contactId, userOrGroupId From ContactShare Where userOrGroupId In :groupMap.values()]) {
      if (existingGroupSharingRulesMap.get(cs.userOrGroupId) == null) {
        existingGroupSharingRulesMap.put(cs.userOrGroupId, new Map<Id, ContactShareMatch>());
      }
      ContactShareMatch csm = new ContactShareMatch();
      csm.contactShareId = cs.Id;
      csm.matchFound = false;
      csm.rule = cs;
      existingGroupSharingRulesMap.get(cs.userOrGroupId).put(cs.contactId, csm);
    }
    return existingGroupSharingRulesMap;
  }

  private static Map<String, Map<Id, GroupMemberMatch>> getExistingGroupMembers(List<User> userList) {
    Map<String, Map<Id, GroupMemberMatch>> groupMemberMap = new Map<String, Map<Id, GroupMemberMatch>>();
    for (GroupMember gm : [SELECT groupId, group.name, userOrGroupId FROM GroupMember WHERE userOrGroupId IN :userList OR group.developerName = 'All_Colleges_Universities']) {
      if (groupMemberMap.get(gm.groupId) == null) {
        groupMemberMap.put(gm.groupId, new Map<Id, GroupMemberMatch>());
      }
      GroupMemberMatch gmm = new GroupMemberMatch();
      gmm.groupMemberId = gm.Id;
      gmm.matchFound = false;
      groupMemberMap.get(gm.groupId).put(gm.userOrGroupId, gmm);
    }
    return groupMemberMap;
  }

  public class GroupMemberMatch {
    public Id groupMemberId {get; set;}
    public Boolean matchFound {get; set;}
  }

  public static void userPermissionsForCommunity() {
    Id collegeRT = Schema.SObjectType.Account.getRecordTypeInfosByName().get('College/University').getRecordTypeId();
//    list<User> userList = [SELECT Id, contact.accountId, contactId, contact.account.recordtypeId, contact.account.name, profile.Id FROM User WHERE contactId != null And profile.Name != 'DSF - Advisor (external)'];
//    The statement above used the profile 'DSF - Advisor (external)', which no longer indicates a partner user. 
    list<User> userList = [SELECT Id, Name, contact.accountId, contactId, contact.account.recordtypeId, contact.account.name, profile.Id 
                           FROM User WHERE contactId != null And profile.Name LIKE 'College - %'];
    Group allUniGroup = [SELECT ID FROM Group WHERE developerName = 'All_Colleges_Universities'];
    Map<String, Group> groupMap = getUniversityGroups();

    List<Group> toCreate = new List<Group>();
    for (User u : userList) {
      system.debug('User: '+u.Name+', Uni: '+u.Contact.Account.Name);
      system.debug('Account Record Type: ' + u.Contact.Account.recordtypeId);
      system.debug('Account ID: ' + u.contact.accountId);
      system.debug('Group Map lookup: ' + groupMap.get('grp_' + u.contact.accountId));
                   
      if (u.contact.account.recordtypeId == collegeRT && u.contact.accountId != null && groupMap.get('grp_' + u.contact.accountId) == null) {
        Group newGroup = new Group();
        newGroup.type = 'Regular';
        newGroup.name = u.contact.account.name;
        newGroup.doesSendEmailToMembers = false;
        newGroup.doesIncludeBosses = true;
        newGroup.developerName = 'grp_' + u.contact.accountId;
        groupMap.put(newGroup.developerName, newGroup);
        toCreate.add(newGroup);
        system.debug('MAKING GROUP ' + newGroup);
      }
    }
    insert toCreate;

    Map<String, Map<Id, GroupMemberMatch>> memberMap = getExistingGroupMembers(userList);
    List<GroupMember> toDelete = new List<GroupMember>();
    List<GroupMember> newMemberList = new List<GroupMember>();
    for (User u : userList) {
      if (u.contact.account.recordtypeId == collegeRT && u.contact.accountId != null && groupMap.get('grp_' + u.contact.accountId) != null) {

        GroupMemberMatch existingGroupMembershipFound = null;
        if (memberMap.get(groupMap.get('grp_' + u.contact.accountId).Id) != null) {
          existingGroupMembershipFound = memberMap.get(groupMap.get('grp_' + u.contact.accountId).Id).get(u.Id);
        }

        if (existingGroupMembershipFound == null) {
          GroupMember gm = new GroupMember();
          gm.userOrGroupID = u.Id;
          gm.groupId = groupMap.get('grp_' + u.contact.accountId).Id;
          newMemberList.add(gm);
          if (memberMap.get(groupMap.get('grp_' + u.contact.accountId).Id) == null) {
            memberMap.put(groupMap.get('grp_' + u.contact.accountId).Id, new Map<Id, GroupMemberMatch>());
          }
          GroupMemberMatch gmm = new GroupMemberMatch();
          gmm.matchFound = true;
          memberMap.get(groupMap.get('grp_' + u.contact.accountId).Id).put(u.Id, gmm);
        } else {
          memberMap.get(groupMap.get('grp_' + u.contact.accountId).Id).get(u.Id).matchFound = true;
        }

        //Add new group to "all colleges" group for reporting access
        if (allUniGroup != null && (memberMap.get(allUniGroup.Id) == null || memberMap.get(allUniGroup.Id).get(u.Id) == null)) {
          GroupMember gg = new GroupMember();
          gg.userOrGroupId = u.Id;
          gg.groupId = allUniGroup.Id;
          newMemberList.add(gg);
          if (memberMap.get(allUniGroup.Id) == null) {
            memberMap.put(allUniGroup.Id, new Map<Id, GroupMemberMatch>());
          }
          GroupMemberMatch gmm = new GroupMemberMatch();
          gmm.matchFound = true;
          memberMap.get(allUniGroup.Id).put(u.Id, gmm);
        } else {
          memberMap.get(allUniGroup.Id).get(u.Id).matchFound = true;
        }
      }
    }

    for (Map<Id, GroupMemberMatch> groupMembers : memberMap.values()) {
      for (GroupMemberMatch membership : groupMembers.values()) {
        if (!membership.matchFound && membership.groupMemberId != null) {
          GroupMember gm = new GroupMember();
          gm.Id = membership.groupMemberId;
          toDelete.add(gm);
        }
      }
    }
    delete toDelete;
    insert newMemberList;
  }
}
And finally the test class that is throwing this error on validate. Note that it is using the executeBatchOnDemand method. (SeeAllData=true) is used in order to pull valid Group and Account information.
 
@isTest(SeeAllData=true)
private class AppPermissionBatchService_Test {
  private static List<Contact> createTestContacts(List<Account> accList) {
    Id stRT = Schema.SObjectType.Contact.getRecordTypeInfosByName().get('Student').getRecordTypeId();
    List<Contact> objList = new List<Contact>();
    for (Integer i = 0; i < accList.size(); i++) {
      for (Integer x = 0; x < 5; x++) {
        Contact obj = new Contact();
        obj.lastName = 'Contact ' + i + '-' + x;
        obj.firstName = 'Test';
        obj.recordTypeId = stRT;
        obj.DPSID__c = accList[i].Id;
        objList.add(obj);
      }
    }
    insert objList;
    return objList;
  }

   private static List<Account> getUniAccounts(Integer testSize) {
    Id collRT = Schema.SObjectType.Account.getRecordTypeInfosByName().get('College/University').getRecordTypeId();
    List<Account> objList = [select Id from Account where recordTypeId = :collRT LIMIT :testSize];
    return objList;
  }

  
  private static List<Application__c> createTestApplications(List<Contact> conList) {
    Id stRT = Schema.SObjectType.Contact.getRecordTypeInfosByName().get('Student').getRecordTypeId();
    List<Application__c> objList = new List<Application__c>();
     for (Integer i = 0; i < conList.size(); i++) {
      if (conList[i].recordTypeId == stRT) {
        Application__c obj = new Application__c();
        obj.student_name__c = conList[i].Id;
        obj.school_name__c = conList[i].DPSID__c;
        objList.add(obj);
      }
    }
    insert objList;
    return objList;
  }

  private static User createTestUser(String userProfileName, Contact con) {
    Profile p = [SELECT Id, name from Profile WHERE Name = :userProfileName];
    User u = new User(Alias = 'ajtant', Email = 'community-org@testorg.com',
                      EmailEncodingKey = 'UTF-8', LastName = 'Testing', LanguageLocaleKey = 'en_US',
                      LocaleSidKey = 'en_US', ProfileId = p.Id, contactId = con.Id,
                      TimeZoneSidKey = 'America/Los_Angeles', 
                      UserName = 'community-org' + String.valueOf(Math.Round(Math.Random() * 1000)) + '@testorg.com');
    return u;
  }

  private static Contact createTestContact(Id contactRecordType, Account acc, String fName) {
    Contact con = new Contact(lastName = 'Tester', firstName = fname, accountId = acc.Id, recordTypeID = contactRecordType);
    insert con;
    return con;
  }

  static testMethod void testAppPermissionBatchService() {
    
    map<String, Id> recordMap = new map<String, Id>();
    for (RecordType rt : [SELECT Id, Name FROM RecordType WHERE (sobjecttype = 'Contact') OR (sobjecttype = 'Account')]) {
      recordMap.put(rt.name, rt.Id);
    }
	Map<String, Group> groupMap = CommunitiesUserPermissionService.getUniversityGroups();

    Integer testSize = 5; // Note that testSize squared Applications will be created.
    List<Account> accList = getUniAccounts(testSize);
    Contact con1 = createTestContact(recordMap.get('General'), accList[0],'mainUser');
    User user1 = createTestUser('College - Student Finance', con1);
    List<Contact> conList = createTestContacts(accList);
    List<Application__c> appList = createTestApplications(conList);
    // Create one extra Account to test CommunitiesUserPermissionService code to create new sharing rules.
    Id collRT = Schema.SObjectType.Account.getRecordTypeInfosByName().get('College/University').getRecordTypeId();
    Account extraUni = new Account(name='Test College01',recordTypeId = collRT);
    insert(extraUni);
    Contact extraCon = createTestContact(recordMap.get('General'), extraUni,'extraUser');
    User extraUser = createTestUser('College - Student Finance', extraCon);
    

	
    Test.startTest();
    AppPermissionBatchService.executeBatchOnDemand(10000);
//    System.schedule('Application based Permissions Batch Job', '0 0 0/1 1/1 * ? *', new AppPermissionBatchService());
    Test.stopTest();
    List<ContactShare> chkShare = [Select Id, contactId, userOrGroupId From ContactShare where contactId in :conList];
    System.debug('chkShare:' + chkShare);
    System.assertEquals(testSize * testSize, chkShare.size());

   }
}




 
BritishBoyinDCBritishBoyinDC
So just to explain the error - when you are running a batch in a test context, it can only fire the execution method once. There are two ways to avoid this. Typically, you just create less test records than the scope (up to 200) , so that the batch only fires once.
But when you use SeeAllData that won't work. So if you must use SeeAllData (and I would really try and see if you can avoid that), then the other way is to limit the records that are returned by the QueryLocator start method to only return records in a test context that will fit within the scope, which I assume is 200, To do that, I would usually create some test records with an identifieable name, and then add something to the query using Test.isRunningTest() so you know to add this just for tests.
so somthing like
if (Test.isRunningTest() ) {
' AND School_Name__c = 'MyTestSchool ';
}


To make that work, you might have to change the query to be a dynamic query using a string, but the bind variable will still work with that format.