So you want to mix dml inserts and make callouts in your tests? Thats Cray Cray!

Here’s the low down on how to get around the “You have uncommitted changes pending please commit or rollback…” when trying to mix DML and HTTPCallouts in your test methods.

First, a little background and a health and safety warning. Sooner or later you’ll be faced with testing a method that both a: manipulates existing data, and b: calls out to a third party service for more information via HTTP.  Sadly, this is one of those situations where testing the solution is harder than the actual solution. In a testing situation, you should be inserting your data that your method is going to rely on. But this making a DML call — insert — will prevent any further http callouts from executing within that Apex context. Yuck. That means inserting say, an account, and then making a call out with some of that data … well that just won’t work. No Callouts after a DML call.

So lets cheat a bit. Apex gives us two tools that are helpful here. The first is the @future annotation. Using the @future annotation and methodology allows you to essentially switch apex contexts, at the cost of synchronous code execution. Because of the Apex context switch, governor limits and DML flags are reset. Our second tool is a two-fer of Test.startTest() and Test.stopTest(). (you are using Test.startTest() and Test.StopTest() right?) Among their many tricks is this gem: When you call Test.stopTest(); all @future methods are immediately executed. When combined together these two tricks give us a way to both insert new data as part of our test, then make callouts (which we’re mocking of course) to test, for example, that our callout code is properly generating payload information etc. Here’s an example:

//In a class far far away…
@future
global static void RunMockCalloutForTest(String accountId){
     TestRestClient trc = new TestRestClient();
     id aId;
     try {
          aId = (Id) accountId;
     } catch (Exception e) {
          throw new exception(‘Failed to cast given accountId into an actual id, Send me a valid id or else.’);
     }
     Account a = [select id, name, stuff, foo, bar from Account where id = :aId];

     //make your callout
     RestClientHTTPMocks fakeResponse = new RestClientHTTPMocks(200, ‘Success’, ‘Success’,  new Map<String,String>());
     System.AssertNotEquals(fakeResponse, null);
     Test.setMock(HttpCalloutMock.class, fakeResponse);
     System.AssertNotEquals(trc, null); //this is a lame assertion. I’m sure you can come up with something useful!
     String result = trc.get(‘http://www.google.com’);

}

//In your test…
@isTest
static void test_method_one() {

     //If you’re not using SmartFactory, you’re doing it way too hard. (and wrong)
     Account account = (Account)SmartFactory.createSObject(‘Account’);
     insert account;
     Test.startTest();
     MyFarawayClass.RunMockCalloutForTest(account.id);
     Test.StopTest();
}

This test works, because we can both a: switch to an asynchronous Apex context that’s not blocked from making HTTP Callouts, and b: force that asynchronous Apex context to execute at a given time with test.stopTest().

Leave a Reply

Your email address will not be published. Required fields are marked *