Testing a Database.Batchable implementation

I have some processing code that makes use of batch Apex to avoid hitting governor limits. Running a representative test generates this error:

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.

Testability is an important feature but sadly missing in a few areas of Force.com including this one…

The normal work-around is suggested in the error message. But using that means that the code does not get tested across a batch boundary: such testing is particularly important for Database.Batchable implementations that also implement Database.Stateful to maintain state across batches.

My processing code also makes use of batch chaining where in the finish method of the Database.Batchable sometimes a further instance is created and executed. Unfortunately (but not surprisingly) that chained execution also generates the above error and there is no easy work-around.

Below is a small class I created to work-around both these problems. Instead of invoking Database.executeBatch, invoke BatchableExecutor.executeBatch. When called from a test, this method makes the start/execute(s)/finish pattern of calls synchronously itself and so avoids the above errors. As long as the test uses a small batch size e.g. 3 and makes sure a moderate number of records are returned by the start method e.g. 10 the Database.Batchable logic can be pretty fully tested without hitting any governor limits.

/*
 * Allows basic testing of a Database.Batchable using more than one batch.
 */
public class BatchableExecutor {
    
    private static final String KEY_PREFIX = AsyncApexJob.SObjectType.getDescribe().getKeyPrefix();
    
    public static Id executeBatch(Database.Batchable<SObject> batchable, Integer scopeSize) {
        
        if (!Test.IsRunningTest()) {
            return Database.executeBatch(batchable, scopeSize);
        } else {
            return executeBatchSynchronously(batchable, scopeSize);
        }
    }
    
    private static Id executeBatchSynchronously(Database.Batchable<SObject> batchable, Integer scopeSize) {
        
        // Fake implementation of this interface could be added as neeed
        Database.BatchableContext bc = null;
        
        // Invoke start (assumes QueryLocator is being used)
        Database.QueryLocator start = (Database.QueryLocator) batchable.start(bc);
        Database.QueryLocatorIterator iter = start.iterator();
        List<SObject> sobs = new List<SObject>();
        try {
            // Invoke execute
            while(iter.hasNext()) {
                sobs.add(iter.next());
                if (sobs.size() == scopeSize) {
                    // These calls could be wrapped in try/catch too for negative tests
                    batchable.execute(bc, sobs);
                    sobs.clear();
                }
            }
            if (sobs.size() > 0) {
                batchable.execute(bc, sobs);
            }
        } finally {
            // Invoke finish
            batchable.finish(bc);
        }
        
        // Fake id
        return KEY_PREFIX + '000000000000';
    }
}

Bear in mind that this code does an OK job of emulating the happy path only (as demonstrated by the test below). Also it obviously cannot emulate the transactions, asynchronous execution and object lifecycle of the real mechanism.

@isTest
private class BatchableExecutorTest {
    
    private class Fixture {
        List<Account> accounts = new List<Account>();
        Fixture addAccounts(Integer objectCount) {
            for (Integer i = 0; i < objectCount; i++) {
                accounts.add(new Account(Name = 'target-' + i, Site = null));
            }
            insert accounts;
            return this;
        }
        Fixture execute(Boolean useDatabaseExecuteBatch, Integer batchSize) {
            Test.startTest();
            // Test that both mechanisms produce the same results
            Id jobId = useDatabaseExecuteBatch
                    ? Database.executeBatch(new BatchableExecutorTestBatchable(), batchSize)
                    : BatchableExecutor.executeBatch(new BatchableExecutorTestBatchable(), batchSize)
                    ;
            Test.stopTest();
            System.assertNotEquals(null, jobId);
            return this;
        }
        Fixture assert(Integer expectedBatches) {
            System.assertEquals(1, [select Count() from Account where Name = 'start']);
            System.assertEquals(expectedBatches,  [select Count() from Account where Name = 'execute']);
            System.assertEquals(1,  [select Count() from Account where Name = 'finish']);
            System.assertEquals(accounts.size(), [select Count() from Account where Name like 'target-%' and Site = 'executed']);
            return this;
        }
    }
    
    @isTest
    static void batchableExecutorExecuteBatch() {
        new Fixture().addAccounts(10).execute(false, 3).assert(4);
    }
    
    @isTest
    static void databaseExecuteBatch() {
        new Fixture().addAccounts(10).execute(true, 10).assert(1);
    }
}
// Has to be a top-level class
public class BatchableExecutorTestBatchable implements Database.Batchable<SObject>, Database.Stateful {
    
    public Database.QueryLocator start(Database.BatchableContext bc) {
        Database.QueryLocator ql = Database.getQueryLocator([select Name from Account order by Name]);
    	insert new Account(Name = 'start');
        return ql;
    }
    
    public void execute(Database.BatchableContext bc, List<SObject> scope) {
    	for (SObject sob : scope) {
    		Account a = (Account) sob;
            a.Site = 'executed';
        }
        update scope;
        insert new Account(Name = 'execute');
    }
    
    public void finish(Database.BatchableContext bc) {
    	insert new Account(Name = 'finish');
    }
}
About these ads

One thought on “Testing a Database.Batchable implementation

  1. Another pattern to avoid using Test.isRunningTest() is to implement your business logic behind an interface. Test the business logic separately, and test the batch class using mock classes which implement the interface. It is especially useful if you have multiple routines which all implement some form of chaining and can all work using a generic batch class. Also mock callout classes using Test.setMock() do not work in a batch tests, another reason to use this pattern.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s