12

Suppose I have the following:

global class CMBatchRegisterUsers implements Database.Batchable<sObject>, Database.AllowsCallouts, Database.Stateful {

    global final String orgID = UserInfo.getOrganizationId();
    global final String query;

    global CMBatchRegisterUsers(){
        this.query = 'Select Id,Email from User where IsActive=true';
        System.debug('Organization ID: ' + this.orgID);
    }

    global HttpRequest buildRequest(String email){
        HttpRequest req = new HttpRequest();
        req.setEndpoint('https://www.example.com?user_email=' + email);
        req.setMethod('GET');
        req.setTimeout(60000);
        return req;
    }

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

    global void execute(Database.BatchableContext bc, List<User> scope){
        HttpRequest req;
        Http http;
        for(User u: scope){
            http = new Http();
            req = this.buildRequest(u.Email);
            http.send(req);
        }
    }

    global void finish(Database.BatchableContext bc){}
}

So far I can test 50% of this code however when I try executing the batch as shown below, I always get System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out.

static testMethod void testBatchRegisterUsers(){
        CMBatchRegisterUsers c = new CMBatchRegisterUsers();
        System.assertEquals(c.orgID, UserInfo.getOrganizationId());

        HttpRequest r = c.buildRequest('[email protected]');
        System.assertEquals(r.getEndpoint(), 'https://[email protected]');
        System.assertEquals(r.getMethod(), 'GET');

        Test.StartTest();
        Test.setMock(HttpCalloutMock.class, new CMMockHttpRegistrationResponseGenerator());
        Database.executeBatch(new CMBatchRegisterUsers());
        Test.StopTest();
}

The error seems to be thrown exactly where I do the http.send(req);.

Can someone point out the proper way to test a batch with callouts in it? My Mock callout doesn't seem to be helping. Thanks.

EDIT Here is the Mock generator as requested.

@isTest
global class CMMockHttpRegistrationResponseGenerator implements HttpCalloutMock {

    global HTTPResponse respond(HTTPRequest req) {
        // Create a fake response
        HttpResponse res = new HttpResponse();
        res.setHeader('Content-Type', 'application/json');
        res.setBody('[{"success":true}]');
        res.setStatusCode(200);
        return res;
    }
}

PS. If I change the executeBatch to Database.executeBatch(new CMBatchRegisterUsers(), 1); it throws the 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.

6

1 Answer 1

10

When testing HTTP callouts in Apex, I generally have a Helper Class, with static methods which returns the response I am expecting from the webservice, and in my code, where the http.send(); would normally go, I use this instead.

HTTPResponse resp;
if(!Test.isRunningTest())
{
     resp = http.send(req);
}
else
{
     resp = MockApiCalls.WebserviceExpectedResponse();
}

I know this isn't particularly clean, but unfortunately this seems to be the only way to properly test http callouts for now. There seem to be improvement coming in the future, in the last release they included Testing Web Service Callouts but you still get the problems you are facing

System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out

This is because in test deployments, all tests still run in one context, and you run into the Cannot do a DML statement before doing a webservice callout problem.

Second Option

This only works when you only have 1 webservice callout to test in your entire test code.

You can name your test class which does the webservice testing AAATestClass. Which causes your test to be executed before all the rest, and you don't run into the earlier mentioned problem. This is because tests are ordered by test class alphabetically and executed in that order.

6
  • Nice, i used this trick and it seems to be working! I hope they release that fix soon, thanks! May 12, 2013 at 21:27
  • As mentioned by PJC, the Summer 13 release notes make reference to this exact issue on page 234 and appear to suggest that this problem goes away if you follow their example...but the example they give isn't 100% clear in my view! May 12, 2013 at 22:41
  • So as an update - I have a sandbox on summer 13, and I still can't get this to work...looks like you can make a different async call as part of a test, and then a make a callout, but this doesn't seem to address the need to test an async call that makes a callout using a mockclass (like example above) - I am still getting uncommitted error... May 12, 2013 at 23:31
  • Your uncommitted error means that you are trying to do a DML statement (insert, update, upsert, delete) BEFORE doing a webservice callout, in the SAME CONTEXT. Try to figure out where the DML comes from, and put it after the webservice callout.
    – pjcarly
    May 13, 2013 at 10:42
  • Summer 13 does seem to fully fix the DML problem (It actually seemed to be working with API 27) so long as the MockCallOut request is contained within test.StartTest/StopTest methods. I have a test that does that and works fine. But if I create a batch within my test, try to set a mockcallout and then execute the batch that makes a callout, then I still get the uncommitted error, which is a shame... May 13, 2013 at 16:18

You must log in to answer this question.

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