1

I am attempting to test a @future callout class that pings another class to make an API call and update some fields in the Lead object. I am using the mock http method and here's the script:

@isTest
private class MMarkAPITest
{
    @isTest
    static void MMarkAPIUnitTest() {

        //create a lead with website address of testapp.com
        Lead l = new Lead
        (company = 'Test Company',
        status = 'Open',
        website = 'testapp.com',
        firstname = 'Guy',
        lastname = 'Test');
        insert l;

        Test.startTest();
        //set-up a mock callout
        Test.setMock(HttpCalloutMock.class, new MMarkAPITestHttpCalloutMock());
        // Call method to test.
        // This causes a fake response to be sent
        // from the class that implements HttpCalloutMock.

        MMarkAPI.MMarkAPIFutureCallUpdater('testapp.com');

        Test.stopTest();
        //access the lead entity
        Lead updatedLead = [select Name, Country, Description from Lead where Id = :l.Id];

        System.debug('lead is ' +updatedLead);

        System.assertEquals(l.description, 'description');
        System.assertEquals(l.MZ_Lo__c, 'country');
        System.assertEquals(l.YY_Totag__c, 'total funding');

    }
}

However when running it with System.assertEquals it fails (all assertions return null value).

On the other hand, when I remove the System.assertEquals the test succeeds but also returns the following message:

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

I came across some information regarding DML queries and testing callouts (e g here), but I am struggling to apply it to this problem.

Adding the FutureCallUpdater:

/** CALL FOR THE INSERT UPDATE TRIGGER  **/
@future(callout=true)
public static void MMAPIFutureCallUpdater(string domain){
    MMAPIUpdater(domain);
}

This triggers that:

public static void MMAPIUpdater(string domain){
    list<Lead> leads = [SELECT MM_Description__c FROM Lead WHERE Website LIKE :domain limit 1];

    HttpResponse res = MMAPIcallout(domain);

    if(res.getStatusCode() == 200 && string.isNotBlank(res.getBody())){

      string body = res.getBody();
      //system.debug('body before replaced is'+body);
      body = body.replace('"currency":', '"deal_currency":');
      //system.debug('body after replaced is'+body);

      JSON2Apex companyProfile = MMAPIParse(body);
      system.debug('company profile is' +companyProfile);

      //single ocurring variables
      string description = companyProfile.data.description;
      //system.debug('description is'+description);
      string country = companyProfile.data.country;
      //system.debug('location is'+country);
      string employees_current = companyProfile.data.employees;
      //system.debug('employees_current is'+employees_current);
      string employees_month_ago = companyProfile.data.employees_month_ago;
      //system.debug('employees_month_ago is'+employees_month_ago);
      string employees_6_months_ago = companyProfile.data.employees_6_months_ago;
      //system.debug('employees_6_months_ago'+employees_6_months_ago);
      string total_funding = companyProfile.data.total_funding;
      //system.debug('total_funding is'+total_funding);
      string last_funding_amount = companyProfile.data.last_funding_amount;
      //system.debug('last_funding_amount is'+last_funding_amount);
      string last_funding_date = companyProfile.data.last_funding_date;
      //system.debug('last_funding_date is'+last_funding_date);

      //convert list variables into strings

      //business_models
      List <String> businessModels = companyProfile.data.business_models;
      string commaSeparatedBusinessModels = '';
      if(businessModels != null){
        //system.debug('the business models list it' +business_models);
        for (String model:businessModels ){
          commaSeparatedBusinessModels += model + ', ';
        }
        //system.debug('comma separted business models are'+commaSeparatedBusinessModels);
      }

      //acquired_by
      List <Meta> acquiredBy = companyProfile.data.acquired_by;
      string commaSeparatedAcquiredBy = '';
      if(acquiredBy != null){
        for (Meta acquirer:acquiredBy){
          commaSeparatedAcquiredBy += acquirer + ', ';
        }
      }

      // singular variables to be inserted into Lead objects
      for(Lead l:leads){
        l.MM_Business_Models__c = commaSeparatedBusinessModels;
        l.MM_Description__c = description;
        l.MM_Location__c = country;
        l.MM_employees_current__c = employees_current;
        l.MM_employees_1M_Ago__c = employees_month_ago;
        l.MM_employees_6M_Ago__c = employees_6_months_ago;
        l.MM_Total_Funding__c = total_funding;
        l.MM_Last_Funding_Amount__c = last_funding_amount;
        l.MM_Last_Funding_Date__c = last_funding_date;
        l.MM_Acquirers__c = commaSeparatedAcquiredBy;
      }

      try {
        update leads;
      }   catch (DmlException e) {
        System.debug(e.getMessage());
      }

      //lists of variables to be inserted into custom objects

      /*


      MM_Funding_History__c

      To ensure deals are added only once (no duplicates), the script invokes the lead with the relevant ID number
      (obtained by looking for a lead with the relevant domain) and creates a string of funding deal dates (dealDatesInString)
      separated by a comma. Subsequently, upon lead update only funding rounds with the date
      not appearing on the list are added to the Lead (if (!dealDatesInString.contains(deal.funding_date))

      */

      string LeadId = getLeadID(domain);
      List <Funding> funding = companyProfile.data.funding;
      List<MM_Funding_History__c> dealsList = [SELECT MM_Date__c FROM MM_Funding_History__c WHERE MM_Funding_History_To_Lead__c = :LeadId];
      //system.debug('the list is' +dealsList);

      //ensuring that the deals are added only once
      string dealDatesInString = '';
      for (MM_Funding_History__c d :dealsList){
        dealDatesInString += d + ', ';
      }
      //system.debug('deal dates string is' +dealDatesInString);

      for(Funding deal:funding){
        //ensuring deals are added only if they're not already on the list (using MM_Date__c as unique identifier)
        if (!dealDatesInString.contains(deal.funding_date)){
          MM_Funding_History__c row = new MM_Funding_History__c(
          MM_Funding_History_To_Lead__c = getLeadID(domain),
          MM_Amount__c = deal.amount,
          MM_Date__c = deal.funding_date,
          MM_Funding_Type__c = deal.series,
          MM_Investors__c = deal.investors,
          MM_Currency__c = deal.deal_currency);
          //system.debug('this is row' +row);
          insert row;
        }
      }

    }
  }
  • 1
    Can you post MMarkAPIFutureCallUpdater – EricSSH Jun 13 '17 at 16:58
  • You need to query all fields which you want to check in assert method. I can see you are querying some fields but those are being assigned into 'updatedLead' variable, but in assert method you are using old instance variable to check. – RSG Jun 13 '17 at 17:12
  • @RSG that's valid, but not the problem right now – EricSSH Jun 13 '17 at 17:22
  • I've hit issues before (actually today again) where other things get triggered by the stoptest, causing DML to fire, and giving you that error. e.g. I had a test fail for the same reason as yours because workflow was creating a task on creation of a record - and for some reason I can't discover, that was causing DML to happen. Disable the workflow, and it worked. There have also been historical issues with email alerts causing the uncommitted error in tests. So I would look at other actions that happen on create lead, and see if you can track down the rogue DML – BritishBoyinDC Jun 14 '17 at 0:02
  • 1
    @BritishBoyinDC - I've seen this as well with ProcessBuilder seemingly executing as part of the Test.stoptest and hence in the same transaction as the future causing the UncommittedWorkpending error. One expected the Process Builder to run before test.stoptest. Workaround was to avoid executing the process builder flow in the testmethod by avoiding its entry conditions – cropredy Jun 15 '17 at 17:23
0

There were several Salesforce issues related to

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

Besides of the issues mentioned, additional issues that I have encountered with, were:

  1. The problem with @future(Callout=true) methods. First time I reported this was at February 12, 2015 Spring 15 Release (v.33)

  2. There was a known issue, which was claimed to be fixed in the next year, however, it wasn't really fixed. I have opened another Salesforce support case, they told me the work id of it was "W-3171561" but they never provided a list to known issue and I was promised to be notified once this issue is resolved, however, I was never notified on this issue resolution.

  3. Later I suggested a workaround for this long-running issue to downgrade the apex class version to 28.

  4. As of Summer 2019 release (version 46) I can't reproduce the future callout issue anymore. However, I have found another issue, the reasons for which are not clear to me. It is somehow related to user trigger and batch or queueable jobs. I have opened another Salesforce support case for this. Let's see how this goes. I have posted about this issue today on my blog

  5. The issue with user insert and future seems to be fixed for future jobs but not for queueable jobs.

Conclusion.

There is some constant long-running ever-lasting bug related to uncommitted work pending. It is hard to find and reproduce it. Every time when Salesforce makes some change to this implementation, it becomes harder to debug and to troubleshoot it, but the core issue is still present.

Some workaround might be applicable for this issue.

  1. Try to downgrade the class version to 28.0 or older where issue wasn't introduced yet (the version should be older than Spring 15).

  2. Try to move the setup code into method annotated with @testSetup annotation. It is not clear why, however, in many cases this helps to workaround this issue

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.