0

System.UnexpectedException: No more than one executeBatch can be called from within a test method. Please make sure the iterable returned from your start method matches the batch size, resulting in one executeBatch invocation.

Apex Class

global class PopulateApproversBatch implements Database.Batchable<sObject>, Database.Stateful {
    private Set<Id> processedUserIds = new Set<Id>();

    global Database.QueryLocator start(Database.BatchableContext BC) {
        return Database.getQueryLocator([
            SELECT Id, Name, Employee_Manager__c, Employee_VP__c, Employee_SVP__c
            FROM User
            WHERE isActive = true
            AND (Employee_Manager__c != null OR Employee_VP__c != null OR Employee_SVP__c != null)
        ]);
    }

    global void execute(Database.BatchableContext BC, List<User> scope) {
        Set<Id> managerVpSvpIds = new Set<Id>();
        for (User user : scope) {
            collectUserIds(user, managerVpSvpIds);
        }

        if (!managerVpSvpIds.isEmpty()) {
            Map<Id, User> userMap = new Map<Id, User>([SELECT Id, Name FROM User WHERE Id IN :managerVpSvpIds]);

            Set<Id> existingApproverIds = new Set<Id>();
            for (AggregateResult result : [
                SELECT sbaa__User__c
                FROM sbaa__Approver__c
                WHERE sbaa__User__c IN :managerVpSvpIds
                GROUP BY sbaa__User__c
            ]) {
                existingApproverIds.add((Id)result.get('sbaa__User__c'));
            }

            List<sbaa__Approver__c> approverRecordsToInsert = new List<sbaa__Approver__c>();
            for (Id userId : managerVpSvpIds) {
                if (!existingApproverIds.contains(userId)) {
                    User user = userMap.get(userId);
                    sbaa__Approver__c approverRecord = new sbaa__Approver__c(
                        sbaa__User__c = userId,
                        Name = user.Name
                    );
                    approverRecordsToInsert.add(approverRecord);
                }
            }

            if (!approverRecordsToInsert.isEmpty()) {
                try {
                    insert approverRecordsToInsert;
                } catch (DMLException e) {
                    System.debug('Error inserting Approver records: ' + e.getMessage());
                }
            }
        }
    }

    private void collectUserIds(User user, Set<Id> managerVpSvpIds) {
        if (user.Employee_Manager__c != null && !processedUserIds.contains(user.Employee_Manager__c)) {
            managerVpSvpIds.add(user.Employee_Manager__c);
            processedUserIds.add(user.Employee_Manager__c);
        }
        if (user.Employee_VP__c != null && !processedUserIds.contains(user.Employee_VP__c)) {
            managerVpSvpIds.add(user.Employee_VP__c);
            processedUserIds.add(user.Employee_VP__c);
        }
        if (user.Employee_SVP__c != null && !processedUserIds.contains(user.Employee_SVP__c)) {
            managerVpSvpIds.add(user.Employee_SVP__c);
            processedUserIds.add(user.Employee_SVP__c);
        }
    }

    global void finish(Database.BatchableContext BC) {
        // Optional: Add any post-processing logic here
    }
}

Test Class

@isTest
private class PopulateApproversBatchTest {

    private static final Integer NUM_TEST_USERS = 5;
    private static Id standardUserProfileId;
   

    @TestSetup
    static void setupTestData() {
        // Get the Standard User Profile Id only once
        if (standardUserProfileId == null) {
            standardUserProfileId = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1].Id;
        }

        // Create test data for User records
        List<User> testUsers = new List<User>();

        for (Integer i = 0; i < NUM_TEST_USERS; i++) {
            String uniqueUsername = 'test' + i + '@testdomain.com'; // Generate a unique username

            User testUser = new User(
                FirstName = 'FirstName' + i,
                LastName = 'LastName' + i,
                Email = 'test' + i + '@example.com',
                Username = uniqueUsername, // Assign the unique username
                Alias = 'test' + i,
                EmailEncodingKey = 'UTF-8',
                LanguageLocaleKey = 'en_US',
                LocaleSidKey = 'en_US',
                TimeZoneSidKey = 'America/Los_Angeles',
                ProfileId = standardUserProfileId,
                IsActive = true
            );
            testUsers.add(testUser);
        }

        // Assign managers, VPs, and SVPs
        for (Integer i = 0; i < NUM_TEST_USERS; i++) {
            User user = testUsers[i];
            if (i < 2) {
                user.Employee_Manager__c = testUsers[2].Id;
            } else if (i == 2) {
                user.Employee_VP__c = testUsers[3].Id;
            } else if (i == 3) {
                user.Employee_SVP__c = testUsers[4].Id;
            }
        }

        insert testUsers;
    }


    @isTest
    static void testPopulateApproversBatch() {
        Test.startTest();

        // Execute the batch job
        PopulateApproversBatch batchJob = new PopulateApproversBatch();
        Database.executeBatch(batchJob,NUM_TEST_USERS);

        Test.stopTest();

        // Fetch the test User records and expected Approver Ids
        List<User> testUsers = [SELECT Id, Employee_Manager__c, Employee_VP__c, Employee_SVP__c FROM User WHERE IsActive = true];
        Set<Id> expectedApproverIds = new Set<Id>();
        for (User user : testUsers) {
            if (user.Employee_Manager__c != null) {
                expectedApproverIds.add(user.Employee_Manager__c);
            }
            if (user.Employee_VP__c != null) {
                expectedApproverIds.add(user.Employee_VP__c);
            }
            if (user.Employee_SVP__c != null) {
                expectedApproverIds.add(user.Employee_SVP__c);
            }
        }

        // Perform assertions
        Integer expectedApproverRecordCount = expectedApproverIds.size();
        Integer actualApproverRecordCount = [SELECT COUNT() FROM sbaa__Approver__c];
        System.assertEquals(expectedApproverRecordCount, actualApproverRecordCount, 'Incorrect number of approver records created');

        List<sbaa__Approver__c> createdApprovers = [SELECT sbaa__User__c FROM sbaa__Approver__c];
        Set<Id> createdApproverIds = new Set<Id>();
        for (sbaa__Approver__c approver : createdApprovers) {
            createdApproverIds.add(approver.sbaa__User__c);
        }
        System.assert(createdApproverIds.containsAll(expectedApproverIds), 'Missing Approver records for some users');
    }
}

I tried the following without success

        String query = 'SELECT Id, Name, Employee_Manager__c, Employee_VP__c, Employee_SVP__c ' +
                   'FROM User ' +
                   'WHERE IsActive = true ' +
                   'AND (Employee_Manager__c != null OR Employee_VP__c != null OR Employee_SVP__c != null)';
    
          // Set the batch size as the second parameter
          return Database.getQueryLocator(query, 200);

If I modify the Integer NUM_TEST_USERS = 200 in the test class, however the test runs forever.

1 Answer 1

2

User is an example of a setup object. One of the special things about setup objects is that they have access to the actual records of those objects in the org you're running the tests in; even if you aren't using seeAllData=true.

So in addition to the 5 User records you're creating for your test, your start() method is also returning every real User record that matches the filters in your WHERE clause.

Your options to address this are:

  • Change Database.executeBatch(batchJob, NUM_TEST_USERS); in your test method to be the number of Users in your org (obtained from a separate query, assuming you have less than 2,000 Users, which is the maximum batch size)
  • Make an adjustment to the query in your start() method so you can say "hey, we're in a test context, so only query the users inserted for this test"
  • Call the start(), execute(), and finish() methods manually instead of using Database.executeBatch() in your test class

My preference is to go with calling the methods manually because we don't have control (or responsibility) over the mechanism that Salesforce uses to make batch Apex work. We should be able to trust that Salesforce is testing their own features.

In 99% of cases, the code inside of a batch class never actually uses the BatchableContext parameter, so you can just pass null in for that.

1
  • 1
    The latter is my preference as well, unless I absolutely need to be in a "real" batchable context.
    – sfdcfox
    Commented Apr 22 at 16:35

You must log in to answer this question.

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