[SalesForce] Test class get error System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out

I have almost tried all solutions available on internet but i still face the error. my Test class code is

@isTest
private class RegisterTest {
@isTest static void existingUserCheck()  {                
    Registration__c checkInsert = new Registration__c(First_Name__c = 'Nilkamal', Last_Name__c = 'Gotarne', Company__c = 'Xoyal It Services',
                                                      Job_Title__c = 'Salesforce Developer', Email__c = 'nilkamal@xoyal.com', Phone__c = '+917020111572',
                                                      Address_Line_1__c = 'Bund garden, pune 411015', Address_Line_2__c = '', City__c = 'Pune',
                                                      State__c = 'Maharashtra', Country__c = 'India', Zip__c = '411001');    

    insert checkInsert;

    Register CheckRegister = new Register(new ApexPages.StandardController(new Registration__c()));       
    CheckRegister.RegData.First_Name__c = 'Nilkamal';
    CheckRegister.RegData.Last_Name__c = 'Gotarne';
    CheckRegister.RegData.Email__c = 'nilkamal@xoya.com';
    CheckRegister.RegData.Phone__c = '9021567897';
    CheckRegister.RegData.Address_Line_1__c = 'Earth';
    CheckRegister.RegData.Address_Line_2__c = 'Mars' ;
    CheckRegister.RegData.Company__c = 'Xoya';
    CheckRegister.RegData.Job_Title__c = 'Sales Man';
    CheckRegister.RegData.Zip__c = '411015';
    CheckRegister.RegData.City__c = 'Poona';
    CheckRegister.RegData.State__c = 'Maha Rashtra';
    CheckRegister.RegData.Country__c = 'Bharat';    
    Test.startTest();        
    Test.setMock(HttpCalloutMock.class, new MockHttpResponseRegisterInfo());                    
    CheckRegister.saveUserInfo();
    Test.stopTest();        
}
}

My class is

public class Register {
public final Registration__c RegData;
public PageReference saveUserInfo() {                   
   if([Select First_Name__c, Last_Name__c, Company__c, Job_Title__c, Email__c, Phone__c, Address_Line_1__c, Address_Line_2__c, City__c, State__c, Country__c, Zip__c, Total_fund__c From Registration__c Limit 1].size() == 0)
    {            
        Registration__c createRegInfo = new Registration__c(First_Name__c = RegData.First_Name__c, Last_Name__c = RegData.Last_Name__c, Email__c = RegData.Email__c,
                                                           Phone__c = RegData.Phone__c, Address_Line_1__c = RegData.Address_Line_1__c, Address_Line_2__c = RegData.Address_Line_2__c,
                                                           Company__c = RegData.Company__c, Job_Title__c = RegData.Job_Title__c, Zip__c = RegData.Zip__c, City__c = RegData.City__c,
                                                           State__c = RegData.State__c, Country__c = RegData.Country__c);                        

        Http http = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndpoint('http://twil-smsapp.herokuapp.com/api/createRegInfo');
        request.setMethod('POST');
        request.setTimeout(10000);
        request.setHeader('Content-Type', 'application/json;charset=UTF-8');
        // Set the body as a JSON object
        request.setBody('{"regInfo":'+JSON.serialize(createRegInfo)+',"organisationId":"'+UserInfo.getOrganizationId()+'" ,"createdBy":"'+userInfo.getUserId()+'"}');
        //System.debug('{"contactInfo":'+JSON.serialize(contact)+',"message":'+JSON.serialize(mess.Text_Message__c)+',"templateId":"'+template_id+'","createdBy":"'+userInfo.getUserId()+'"}');
        HttpResponse response = http.send(request);
        System.debug('{"regInfo":'+JSON.serialize(createRegInfo)+',"organisationId":'+UserInfo.getOrganizationId()+' ,"createdBy":"'+userInfo.getUserId()+'"}');
        // Parse the JSON response
        if (response.getStatusCode() != 200) {
            System.debug(response.getBody());
            System.debug('The status code returned was not expected: ' +
                response.getStatusCode() + ' ' + response.getStatus());
            ApexPages.AddMessage(new ApexPages.Message(ApexPages.Severity.ERROR,'Oops, something went wrong!!'));
        } else {

            insert createRegInfo;
            ApexPages.AddMessage(new ApexPages.Message(ApexPages.Severity.CONFIRM,'Registered Successfully you can also update below information!'));
            System.debug(response.getBody());
        }

    } else {
        Registration__c createRegInfo = [Select First_Name__c, Last_Name__c, Company__c, Job_Title__c, Email__c, Phone__c, Address_Line_1__c,
                                         Address_Line_2__c, City__c, State__c, Country__c, Zip__c, Total_fund__c FROM Registration__c LIMIT 1 FOR UPDATE];
            createRegInfo.First_Name__c = RegData.First_Name__c;
            createRegInfo.Last_Name__c = RegData.Last_Name__c;
            createRegInfo.Email__c = RegData.Email__c;
            createRegInfo.Phone__c = RegData.Phone__c;
            createRegInfo.Address_Line_1__c = RegData.Address_Line_1__c;
            createRegInfo.Address_Line_2__c = RegData.Address_Line_2__c;
            createRegInfo.Company__c = RegData.Company__c;
            createRegInfo.Job_Title__c = RegData.Job_Title__c;
            createRegInfo.Zip__c = RegData.Zip__c;
            createRegInfo.City__c = RegData.City__c;
            createRegInfo.State__c = RegData.State__c;
            createRegInfo.Country__c = RegData.Country__c;
        update createRegInfo;                      

        Http http = new Http();
        HttpRequest request = new HttpRequest();
        request.setEndpoint('http://twil-smsapp.herokuapp.com/api/createRegInfo');
        request.setMethod('POST');
        request.setTimeout(10000);
        request.setHeader('Content-Type', 'application/json;charset=UTF-8');
        // Set the body as a JSON object
        request.setBody('{"regInfo":'+JSON.serialize(createRegInfo)+',"organisationId":"'+UserInfo.getOrganizationId()+'" ,"createdBy":"'+userInfo.getUserId()+'"}');
        //System.debug('{"contactInfo":'+JSON.serialize(contact)+',"message":'+JSON.serialize(mess.Text_Message__c)+',"templateId":"'+template_id+'","createdBy":"'+userInfo.getUserId()+'"}');
        HttpResponse response = http.send(request);
        System.debug('{"regInfo":'+JSON.serialize(createRegInfo)+',"organisationId":'+UserInfo.getOrganizationId()+' ,"createdBy":"'+userInfo.getUserId()+'"}');
        // Parse the JSON response
        if (response.getStatusCode() != 200) {
            System.debug(response.getBody());
            System.debug('The status code returned was not expected: ' +
                response.getStatusCode() + ' ' + response.getStatus());
            ApexPages.AddMessage(new ApexPages.Message(ApexPages.Severity.ERROR,'Oops, something went wrong!!'));
        } else {
            insert createRegInfo;
            ApexPages.AddMessage(new ApexPages.Message(ApexPages.Severity.CONFIRM,'Registered Successfully!'));
            System.debug(response.getBody());
        }
    }
    //System.debug(regInfo);
    return null;
}
}

Mock callout class

@isTest
global class MockHttpResponseRegisterInfo implements HttpCalloutMock {
// Implement this interface method
global HTTPResponse respond(HTTPRequest req) {
    // Optionally, only send a mock response for a specific endpoint
    // and method.
    //System.assertEquals('http://example.com/example/test', req.getEndpoint());
    //System.assertEquals('GET', req.getMethod());
    // Create a fake response
    HttpResponse res = new HttpResponse();
    res.setHeader('Content-Type', 'application/json');
    res.setBody('{"example":"test"}');
    res.setStatusCode(200);
    return res;
  }
}

Best Answer

At the very least, the error isn't very mysterious at all.

The first thing you're doing in your test class is creating a record and performing a DML insert. In the case where you execute the else bit of your saveUserInfo() method, you're additionally performing an update prior to the callout.

Callouts cannot happen after DML.

There are a couple of other issues with your code (looks like you copy/pasted the if/else handling the callout response to the block that handles when a Registration__c already exists, and are trying to insert a record that already exists).

I'll focus on fixing the "uncommited work pending" error(s). Perhaps the simplest way to fix this is to do a little bit of dependency injection. In a nutshell, when a method needs a certain piece of information, dependency injection is where you pass the required thing(s) into the class or method instead of creating them in the method itself.

A very simple example

public void myMethod(){
    // This data is required for the method to work, and we're creating it
    //   inside the method itself.
    Integer i = 2;
    system.debug(1 + i);
}

public void myMethodWithDependencyInjection(Integer i){
    // Passing the required data as a parameter can make the method more flexible
    //   and easier to test.
    system.debug(1 + i);
}

How does this help you?

Well, the entire reason why you require the DML insert before calling your saveUserInfo() method is because you query to see if there is at least one Registration__c record. If you remove that query, you remove the need for the DML insert in your test method.

public PageReference saveUserInfo(List<Registration__c> registrationList) {                   
    // Instead of querying and seeing if we get any results, just check to see
    //   if the list that was passed in is empty or not
    //if([Select First_Name__c, Last_Name__c, Company__c, Job_Title__c, Email__c, Phone__c, Address_Line_1__c, Address_Line_2__c, City__c, State__c, Country__c, Zip__c, Total_fund__c From Registration__c Limit 1].size() == 0)
    if(registrationList.isEmpty())
    {            
        Registration__c createRegInfo = new Registration__c(
            First_Name__c = RegData.First_Name__c, 
            Last_Name__c = RegData.Last_Name__c, 
            Email__c = RegData.Email__c,
            Phone__c = RegData.Phone__c, 
            Address_Line_1__c = RegData.Address_Line_1__c, 
            Address_Line_2__c = RegData.Address_Line_2__c,
            Company__c = RegData.Company__c, 
            Job_Title__c = RegData.Job_Title__c, 
            Zip__c = RegData.Zip__c, 
            City__c = RegData.City__c,
            State__c = RegData.State__c, 
            Country__c = RegData.Country__c
        );

        // callout code

        // response handling

    } else {
        // Having the list as a parameter allows us to remove this query as well.
        // We can simply pull the record from the list
        //Registration__c createRegInfo = [Select First_Name__c, Last_Name__c, Company__c, Job_Title__c, Email__c, Phone__c, Address_Line_1__c,
                                         Address_Line_2__c, City__c, State__c, Country__c, Zip__c, Total_fund__c FROM Registration__c LIMIT 1 FOR UPDATE];

        createRegInfo = registrationList[0];
        createRegInfo.First_Name__c = RegData.First_Name__c;
        createRegInfo.Last_Name__c = RegData.Last_Name__c;
        createRegInfo.Email__c = RegData.Email__c;
        createRegInfo.Phone__c = RegData.Phone__c;
        createRegInfo.Address_Line_1__c = RegData.Address_Line_1__c
        createRegInfo.Address_Line_2__c = RegData.Address_Line_2__c;
        createRegInfo.Company__c = RegData.Company__c;
        createRegInfo.Job_Title__c = RegData.Job_Title__c;
        createRegInfo.Zip__c = RegData.Zip__c;
        createRegInfo.City__c = RegData.City__c;
        createRegInfo.State__c = RegData.State__c;
        createRegInfo.Country__c = RegData.Country__c;

        // This is the other issue.
        // Since you don't re-query this record, and would update it anyway after
        //   the callout, I'm not sure you need this
        //update createRegInfo;                      

        // callout code

        // response handling
    }
}

With that, you can fix most of the issues you've run into.

Your test method would change to

@isTest static void existingUserCheck()  {                
    Registration__c checkInsert = new Registration__c(
        First_Name__c = 'Nilkamal', 
        Last_Name__c = 'Gotarne', 
        Company__c = 'Xoyal It Services',
        Job_Title__c = 'Salesforce Developer', 
        Email__c = 'nilkamal@xoyal.com', 
        Phone__c = '+917020111572',
        Address_Line_1__c = 'Bund garden, pune 411015', 
        Address_Line_2__c = '', 
        City__c = 'Pune',
        State__c = 'Maharashtra', 
        Country__c = 'India', 
        Zip__c = '411001'
    );

    Register CheckRegister = new Register(new ApexPages.StandardController(new Registration__c()));
    CheckRegister.RegData.First_Name__c = 'Nilkamal';
    CheckRegister.RegData.Last_Name__c = 'Gotarne';
    CheckRegister.RegData.Email__c = 'nilkamal@xoya.com';
    CheckRegister.RegData.Phone__c = '9021567897';
    CheckRegister.RegData.Address_Line_1__c = 'Earth';
    CheckRegister.RegData.Address_Line_2__c = 'Mars' ;
    CheckRegister.RegData.Company__c = 'Xoya';
    CheckRegister.RegData.Job_Title__c = 'Sales Man';
    CheckRegister.RegData.Zip__c = '411015';
    CheckRegister.RegData.City__c = 'Poona';
    CheckRegister.RegData.State__c = 'Maha Rashtra';
    CheckRegister.RegData.Country__c = 'Bharat';
    Test.startTest();        
    Test.setMock(HttpCalloutMock.class, new MockHttpResponseRegisterInfo());
    // Construct a new list, using the curly-bracket syntax to initialize the list
    //   with the record you've already set up
    CheckRegister.saveUserInfo(new List<Registration__c>{checkInsert});
    Test.stopTest();        
}

There are still issues

This won't solve all of the problems in your code (you're effectively duplicating your callout and response handling code, your current approach is likely to break if there is more than a single Registration__c record).

This will also introduce a few new problems like:

  • In the case where the Registration__c record is supposed to exist, my test record won't have an Id, so the update DML will not work
  • You presumably want to call this method from a command button, which means you can't pass an argument directly to the method

Both are valid concerns. My answer is getting pretty long, so I won't go into much detail.

The first could be fixed by using an if(Test.isRunningTest()) block, or by rearranging your code.

Instead of

  • Prepare record
  • Callout
  • Handle response

You could (since your callout doesn't use any data from the Registration__c record)

  • Callout
  • Handle non-200 response code
  • Prepare record in the block that handles the 200 response

The second issue could be solved by having your command button call a method which in turn calls your saveUserInfo() method.