The short hand title for this post might also be System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out
Here’s the problem I recently faced:
- Batchable class that does callouts in the start() method.
- Testmethod needs to setup some test data before the batchable class executes
As you probably know if you are reading this ..
- Testmethods can’t do callouts, you have to mock out the callout response using Test.setMock(..)
- Async tasks like Database.executeBatch() or @future methods don’t execute in a test context until the Test.stopTest() method.
Naively, I constructed this basic testmethod:
insert new Account(name = '00test'); // create test data here
Test.startTest();
Test.setMock(HttpCalloutMock.class, myMultiMockObject); // setup of multiMockObject omitted for clarity
MyBatchableClass bClass = new MyBatchableClass();
Database.executeBatch(bClass,2); // scope is 2
Test.stopTest(); // SFDC executes the async task here
System.assert(...) //verify results
When you execute this test, you get System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out
I then tried to setup the test data in a different context like this:
System.runAs(someUserOtherThanRunningUser) {
insert new Account(name = '00test'); // create test data here
}
Test.startTest();
Test.setMock(HttpCalloutMock.class, myMultiMockObject); // setup of multiMockObject omitted for clarity
MyBatchableClass bClass = new MyBatchableClass();
Database.executeBatch(bClass,2); // scope is 2
Test.stopTest(); // SFDC executes the async task here
System.assert(...) //verify results
When you execute this test, you get System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out
Aargh. SFDC doc as of V30 wasn’t much help here. Much Googling ensued. Thanks to tomlogic for the solution essence.
Here is what you have to do –avoid the use of Database.executeBatch() in the testmethod if one of the batchable class’s start(), execute() or finish() methods does a callout. Thus, you have to invoke the start(), execute(), finish() methods explicitly to verify your batchable class logic and get code coverage.
insert new Account(name = '00test'); // create test data here
Test.startTest();
Test.setMock(HttpCalloutMock.class, myMultiMockObject); // setup of multiMockObject omitted for clarity
Database.BatchableContext bc;
MyBatchableClass bClass = new MyBatchableClass();
// we execute the start() and prepare results for execute()
// in my use case, start() does the callout;
// thus the testmethod mocks the results of the callout (assumed here to be accounts)
// setup of custom Iterable and Iterator not shown
MyIterable itrbl = (MyIterable)bclass.start(); //start() returns an iterable, in my case, a custom iterable. Note the casting
MyIterator itrator = (MyIterator) itrbl.iterator(); // continue simulation of start() by constructing the iterator
List<Account> aScopeList = new List<Account> (); // create scope for execute() by iterating against the result of the mocked callout
while (itrator.hasNext()) {
aScopeList.add(itrator.next());
// Now invoke execute() w/ Account list built via mocked callout invoked by start()
bClass.execute(bc,aScopeList);
// Finally, invoke finish()
bClass.finish(bc);
Test.stopTest(); // SFDC executes the async task here
System.assert(...) //verify results
To recap, this approach accomplishes the following:
- It will test code coverage as start(), execute(), and finish() are all invoked as if SFDC were invoking them by Database.executeBatch().
- It avoids the System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out
- It allows you to test against the returned data from the mocked callout(s) thus isolating your testmethod to predictable responses.
- It tests your custom iterator, if you are using one
- It only requires a bit more setup to individually invoke start(), execute(), and finish() plus manual construction of what execute() would get if database.executeBatch() were called