Here is one approach to consider where for tests the code is run synchronously so Test.stopTest
calls are not needed.
Wherever you invoke:
Database.executeBatch
replace that code with:
BatchableExecutor.executeBatch
that is a local class that looks like this:
public class BatchableExecutor {
private static String KEY_PREFIX {
get {
if (KEY_PREFIX == null) {
KEY_PREFIX = AsyncApexJob.SObjectType.getDescribe().getKeyPrefix();
}
return KEY_PREFIX;
}
set;
}
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';
}
}
The class does a basic emulation of the batching.