1

I have read several document regarding this ,including Testing webservice call out but still I cannot find the solution for my issue.

I am creating test method to test batch class that contain httpcallout method.When I run the test, the result is pass but when I went through the debug log, the code that called callout method such if(statusCode==200) is not covered and got exception like below:

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

Here the batch class

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

String query='Select Id,Name,Status__c,UserId from Transaction__c where Status__c='\'Pending\'';
Map<String, MyObject> mapMyObject  = new Map<String, MyObject>();

public MyBatch(){
         //contructor contain initialization
       }

        global void execute(SchedulableContext sc) {}

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

        global void execute(Database.BatchableContext BC, List<Transaction__c> transactions) {
                  for(Transaction__c t:transactions){
                       mapMyObject.put(t.UserId,new MyObject(Name='Bla bla');
                  }
                  system.debug('mapMyObject size '+mapMyObject.size());

                 for (MyObject o : mapMyObject.Values()){
                   //contain logic and call out
                   MyCallOut.getUserByEmail(email)
                   if(statusCode==200){
                      system.debug('Good job!')
                    }
                  //the rest of logic and call cout
                 }
          }
    }

Here the class that contain HttpCallout method that called by my batch class

global class MyCallOut {
     public static string getUserByEmail(string email){
          String returnStr='';
   try{

    String baseUrl = externalURL+'/2.0/admin/users/'+email;
    system.debug('@getUserByEmail  '+ baseUrl );

   HTTPRequest request = new HTTPRequest();              
    request.setEndpoint(baseUrl);
    String authorizationHeader = 'WRAP access_token=' + token;

    request.setHeader('Authorization', authorizationHeader);
    request.setHeader('Content-Type', 'application/xml');
    request.setMethod('GET');
    request.setTimeout(120000);

    HTTP http = new HTTP();
    HTTPResponse response;
            response =  http.send(request);    
            if(response.getStatusCode()>=400){
               returnStr=response.getStatusCode() +' : '+ response.getStatus();
               errorCode=response.getStatusCode();
               errorMessage=returnStr;
             }

            else {
               errorCode=response.getStatusCode();
               returnStr=response.getBody();
            }               

    system.debug('@returnStr  '+ returnStr);
   }

catch(Exception e){

       errorCode=700;
       errorMessage=e.getMessage();
       errorMessage+= ' ::: inside getUserByEmail(string email)';

}

    return returnStr;
     }

    public static string populateProfile(string str{
             //HttpRequest bla bla
    }

   //more httpcall method    
}

Here is the test method.I am using MultiStaticResourceCalloutMock,also please note that I already put Test.startTest() and Test.stopTest() properly as stated in reference link.

 @isTest
public class MyBatchTest {
static{

URLConfig__c setting = new URLConfig__c();
setting.Name = 'Url';
setting.Value__c = 'https://api.bla.com';
insert setting;

URLConfig__c setting1 = new URLConfig__c();
setting1.Name = 'Key';
setting1.Value__c = 'ABC1234323';
insert setting1;

Id folderId=[Select Id from Folder Where Name='Token'].Id;
Blob bToken=Blob.valueOf('JYYTGHGUTY6567GYT8765gXDWSRET');
Document doc1 =new Document(Name='Token',Body =bToken,ContentType='text/plain',FolderId=folderId);
insert doc1;
List<Document> doc =[SELECT Id,Body,Name FROM Document WHERE  Name='Token'];

System.assertEquals(1, doc.size());

 User newUser=new User(alias = 'newuser', email='newuser@domain.com', 
               Department='HR Management', username='newuser@domain.com');
insert newUser;

 System.AssertEquals(
           database.countquery('SELECT COUNT()'
              +' FROM User  where Id  =  \''+newUser.Id+'\''), 
           1);

      List<Transaction__c> trans = [SELECT Id,Name,UserId__c FROM Transaction__c where UserId__c =: newUser.Id];

      system.debug('@test '+trans.size());
 }
static testMethod  void testBatch(){


    MultiStaticResourceCalloutMock multimock = new MultiStaticResourceCalloutMock();
    multimock.setStaticResource('https://api.bla.com/2.0/admin/users/newuser@domain.com', 'UserProfile_Test');
    multimock.setStatusCode(200);
    multimock.setHeader('Content-Type', 'application/json');

    Test.startTest();
     Test.setMock(HttpCalloutMock.class, multimock);

      MyBatch c = new MyBatch();
      c.query='SELECT Id,Name,UserId__c,Status__c '+
                                       +' FROM Transaction__c WHERE Status__c IN (\'Pending\') LIMIT 1';

     Database.executeBatch(c,1);
     Test.stopTest();

}
}

Thanks for your idea.

2

I think your issue is a known limitation - see my answer here from a while back. I have never found a combination of mock services and test.start/stop that lets you test a batch that contains a callout. I think your best bet is to move the callout to a separate class that you can test independently, and then have the batch check for a running test before calling it

  • I read the your answer and add isRunningTest in callout class to test batch class.However it still prompt the same exception, and I noticed it happen when it hit HTTPResponse response = http.send(request); So instead of using response , I hardcode the xml string directly and return it, it works.The batch class get covered but not call out class.So I tried to create another test class for call out class,but because I already added isRunningTest in this call out class I wonder if it cover call out code because it will always go into isRunningTest=true regardless from which test class I call. – unidha Feb 17 '14 at 10:14
  • So I would use the isRunningTest in the Batch class at the point you make the callout - and if you need a response to come back, retrieve a string at that point. Then you can test the callout with the usual mock interface, and you'll have most of you code covered... – BritishBoyinDC Feb 17 '14 at 17:03
  • Thank you @BritishBoyinDC , I follow your instruction and get it covered.Two test class for each Batch and Callout.Put isRunningTest() at Batch class , for call out class I use MultiStaticResourceCalloutMock .Hope in future I no need to create test class separately like this. – unidha Feb 18 '14 at 9:07
  • Agreed - I think I will raise a case with SF - feels like this should not the way it is working – BritishBoyinDC Feb 18 '14 at 17:21
1

I have noticed many things in your batch.

1) It will hit callout governor limit if your mapMyObject contains more than 10 values.

2) You have called "MyCallOut.getUserByEmail(email)" in for loop so if this method contain any DML call then the order of execution will be callout->DML call->callout->DML call, in this scenario system will trow "System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out". Because DML operations are not allowed before callout. To Avoid this do not do DML in getUserByEmail, instead create a list and call DML in batch itself.

There is other method also for avoiding this exception, instead using httpcalloutmock please see below code.

if(!Test.isRunningTest())
   response="REAL RESPONSE";
else
   response="FAKE RESPONSE";

Hope this helps.

  • Unfortunately I don't have DML. The only DML is outside mapMyObject loop where by I shall do update after I completed all call out and do get set to the object in the list. – unidha Feb 17 '14 at 7:29
  • Can you post what you did in getUserByEmail. – SFIntegrator Feb 17 '14 at 7:50
  • Hi @sandy, I edited question above with code inside getUserByEmail method.Basically the method is to check the user existing in external server , so I need to do it inside the loop because the external webservice only accept one user email.After that I will do logic such as set the object but not DML. I will add the object into the list and update it outside the loop. JFYI, I only set Batch scope equal 1 to prevent too many call out limitation. – unidha Feb 17 '14 at 9:34

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.