1

I have a batch apex class that does what I want it to do but I am currently trying to test it and because I am using some objects from financial force, creating testdata has been a bit complicated. Spoke to support and they recommended using data from the organization itself. I have done this and it works in the sandbox but on deployment I get an error stating 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. Stack Trace: External entry point

This is my test class

@isTest(SeeAllData=true)
public class ServicesBatchTest {
 

   @IsTest
    static void testExecute() {
        // Fetch a small set of existing invoices to use in the test
        
        List<c2g__codaInvoice__c> testInvoices = [SELECT Id, c2g__Account__c, c2g__InvoiceStatus__c, LastModifiedDate 
                                                  FROM c2g__codaInvoice__c 
                                                  WHERE c2g__InvoiceStatus__c = 'Complete' 
                                                  AND LastModifiedDate = TODAY
                                                  LIMIT 5];
        
        // Ensure there is at least one invoice to test
        if (testInvoices.isEmpty()) {
            System.debug('No invoices found with the status Complete and modified today.');
            return;
        }

        // Create an instance of the batch class and call the execute method directly
        Test.startTest();
        ServicesBatch batch = new ServicesBatch();  
        Database.executeBatch(batch, 5);  
        Test.stopTest();


        // Verify the results
        Set<Id> accountIds = new Set<Id>();
        for (c2g__codaInvoice__c invoice : testInvoices) {
            if (invoice.c2g__Account__c != null) {
                accountIds.add(invoice.c2g__Account__c);
            }
        }

        List<Account> updatedAccounts = [SELECT Id, Last_Invoice_Date__c 
                                         FROM Account 
                                         WHERE Id IN :accountIds 
                                         AND Last_Invoice_Date__c != NULL];

        System.assert(!updatedAccounts.isEmpty(), 'Accounts should have been updated with the latest invoice date');

        // Additional assertions to check specific business logic if required
        for (Account acc : updatedAccounts) {
            Date latestInvoiceDate = null;
            for (c2g__codaInvoice__c invoice : testInvoices) {
                if (invoice.c2g__Account__c == acc.Id) {
                    Date invoiceDate = invoice.LastModifiedDate.date();
                    if (latestInvoiceDate == null || invoiceDate > latestInvoiceDate) {
                        latestInvoiceDate = invoiceDate;
                    }
                }
            }
            System.assertEquals(latestInvoiceDate, acc.Last_Invoice_Date__c, 'The Last_Invoice_Date__c should match the latest invoice date for the account.');
        }
    }

    @IsTest
    static void testSchedulable() {
        // Execute the scheduled job
     
        Test.startTest();
        ServicesBatch schedulable = new ServicesBatch();
        String jobId = System.schedule('TestScheduledBatch', '0 0 0 * * ?', schedulable);
        schedulable.execute(null);
        Test.stopTest();

        // Verify that the job has been scheduled
        CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime
                          FROM CronTrigger WHERE Id = :jobId];
        System.assertEquals('0 0 0 * * ?', ct.CronExpression);
        System.assert(ct.NextFireTime != null, 'Scheduled job should have a next fire time');
    }
}

This is the class its for


global class ServicesBatch implements Database.Batchable<sObject>, Schedulable {
    String query;

    global ServicesBatch() {
        // Query to get all Sales Invoice records created today
        query = 'SELECT Id, c2g__Account__c, c2g__InvoiceStatus__c, lastModifiedDate FROM c2g__codaInvoice__c WHERE lastModifiedDate = TODAY AND c2g__InvoiceStatus__c = \'Complete\'';
    }
 


    global Database.QueryLocator start(Database.BatchableContext BC) {
        return Database.getQueryLocator(query);
    }

    global void execute(Database.BatchableContext BC, List<c2g__codaInvoice__c> scope) {
        // Map to hold the latest invoice date for each Account
        Map<Id, Date> accountInvoiceMap = new Map<Id, Date>();

        // Iterate through the invoices in the current batch
        for (c2g__codaInvoice__c invoice : scope) {
            Date invoiceDate = invoice.lastModifiedDate.date();
            if (invoice.c2g__Account__c != null) {
                // Check if the account is already in the map
                
                if (!accountInvoiceMap.containsKey(invoice.c2g__Account__c)) {
                    accountInvoiceMap.put(invoice.c2g__Account__c, invoiceDate);
                } else {
                    // Update the date if this invoice is more recent
                    Date existingDate = accountInvoiceMap.get(invoice.c2g__Account__c);
                    if (invoiceDate > existingDate) {
                        accountInvoiceMap.put(invoice.c2g__Account__c, invoiceDate);
                    }
                }
            }
        }

        // List to hold the accounts to be updated
        List<Account> accountsToUpdate = new List<Account>();

        // Iterate through the map to create Account records with the latest invoice date
        for (Id accountId : accountInvoiceMap.keySet()) {
            Account acc = new Account(Id = accountId, Last_Invoice_Date__c = accountInvoiceMap.get(accountId));
            accountsToUpdate.add(acc);
        }

        // Update the Account records
        if (!accountsToUpdate.isEmpty()) {
            update accountsToUpdate;
        }
    }

    global void finish(Database.BatchableContext BC) {
        // finish code
    }

    global void execute(SchedulableContext SC) {
        if(!Test.isRunningTest()){
          Database.executeBatch(new ServicesBatch());
        }
    }
}

2 Answers 2

2

First, I'd suggest changing your execute(SchedulableContext) as follows:

public void execute(SchedulableContext SC) {
    Database.executeBatch(this);
}

The reason why is that we're going to modify the query that's passed in.

Let's change String query; to:

@TestVisible String query;

This allows us to change the query, but only in the unit test.

Now, we'll go ahead and adjust the test:


@IsTest
static void testSchedulable() {
    // Execute the scheduled job
    ServicesBatch schedulable = new ServicesBatch();
    // Example. You could also use additional filters, etc.
    schedulable.query += ' LIMIT 1'; 
    Test.startTest();
    String jobId = System.schedule('TestScheduledBatch', '0 0 0 * * ?', schedulable);
    Test.stopTest();
    // Validate that the batch worked.
}

Note that SeeAllData=true is a terrible suggestion. Most of the time, Test.loadData can mimic any kind of data you need without relying on SeeAllData=true.

Also, note that it is a Bad Practice™ to use global for any class that doesn't need it. Unless you're intending to package this into a managed package, and you want subscribers to directly use it, it can just be public. In addition, you don't need to check if the scheduled job was scheduled. It's like testing if 2 + 2 = 4. Trust the platform to do its job. Also, the schedulable class can execute the batch, so you really only need one unit test that does both.

3
  • Thank you so much for taking the time to answer this. I used seealldata equals true cause this test had to do with salesinvoices in financial force and its a pain trying to create them to use in the test. I did contact them and they recommended using that. I absolutely wouldnt but have no choice in this case. Commented Jun 29 at 0:57
  • 1
    There is always a choice, if you leverage a mocking framework of some form and (where necessary) abstract away interactions with the DB and objects that cannot be mocked.
    – Phil W
    Commented Jun 29 at 7:06
  • Tried pushing this to production and still had the same error Commented Jul 1 at 13:47
0

In addition to the above I also had to add this to the query

+(Test.isRunningTest()?' LIMIT 5':'');

You must log in to answer this question.

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