8

Apologies ahead of time for the lengthy post, but I'm receiving the following error when attempting to run the test method below. The error occurs when the upsertProject method is called.

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

Test Method

@isTest static void upsertErrorTest() {
    // retrieve project to pass to upsert method
    Project__c p = [SELECT Id, Name FROM Project__c LIMIT 1];
    system.assertEquals('Test Project', p.Name);

    // retrieve replicon project data in apex wrapper
    Replicon.Project project = Replicon.getProjectById(p.Id);

    // instantiate payload to use in mock http request
    String errorPayload = '[insert json payload here]';

    Test.startTest();

    // instantiate the http service mock
    RepliconServiceUpsertMock mock = new RepliconServiceUpsertMock(200, 'Success', errorPayload, null);
    Test.setMock(HttpCalloutMock.class, mock);

    RepliconService service = new RepliconService();
    HttpResponse response = service.upsertProject(project, p.Id);

    // assertions here

    Test.stopTest();
}

Http Callout Method

public HttpResponse upsertProject(Replicon.Project project, String projectId) {
    // serialize the project object for insertion into replicon
    String requestBody = project.toString();

    // instantiate the request to send to replicon
    HttpRequest req = this.getRequest('ImportService1.svc/PutProject4');
    req.setBody(requestBody);

    // send the request to replicon to create a new project
    Http h = new Http();
    HttpResponse response = h.send(req);
    system.debug('[upsertProject] Response:' + response.getBody());

    return response;
}

Test data is created via an @testSetup method in the test class:

@testSetup static void setupData() {
    Account a = new Account();
    a.Name = 'Test Account';
    insert a;

    Opportunity o = new Opportunity();
    o.AccountId = a.Id;
    o.Name = 'Test Opp';
    o.StageName = 'First stage';
    o.CloseDate = system.today();
    o.Amount = 55000.00;
    insert o;

    Project__c p = new Project__c();
    p.Account__c = a.Id;
    p.Opportunity__c = o.Id;
    p.Name = 'Test Project';
    insert p;
}

Mock Class

@isTest
public class RepliconServiceUpsertMock implements HttpCalloutMock {

    protected Integer code;
    protected String status;
    protected String body;
    protected Map<String, String> responseHeaders;

    public RepliconServiceUpsertMock(Integer code, String status, String body, Map<String, String> responseHeaders) {
        this.code = code;
        this.status = status;
        this.body = body;
        this.responseHeaders = responseHeaders;
    }

    public HTTPResponse respond(HTTPRequest req) {

        HttpResponse res = new HttpResponse();
        for(String key : this.responseHeaders.keySet()) {
            res.setHeader(key, this.responseHeaders.get(key));
        }
        res.setBody(this.body);
        res.setStatusCode(this.code);
        res.setStatus(this.status);
        return res;
    }

}

The documentation on using Http callouts with Mocks states:

By default, callouts aren’t allowed after DML operations in the same transaction because DML operations result in pending uncommitted work that prevents callouts from executing. Sometimes, you might want to insert test data in your test method using DML before making a callout. To enable this, enclose the portion of your code that performs the callout within Test.startTest and Test.stopTest statements. The Test.startTest statement must appear before the Test.setMock statement. Also, the calls to DML operations must not be part of the Test.startTest/Test.stopTest block.

DML operations that occur after mock callouts are allowed and don’t require any changes in test methods.

My code is definitely following the order of execution described above. There is also no other processes (e.g. future, scheduled apex, etc) being generated from related triggers. So I'm perplexed as to why I'm receiving this error.

Additionally, I have attempted to move the test data creation into the test method itself and do away with the @testSetup method, but that produced the same results.

Any idea why this is happening?

9
  • Do you have any EMAIL ALERTS on the objects you are creating? If so you will have to use flags/fields to not have them fire during these tests as there is a bug with email alerts: salesforce.stackexchange.com/questions/71259/…
    – Eric
    Commented Jul 29, 2016 at 0:52
  • What do RepliconServiceUpsertMock and RepliconService.upsertProject() look like? Any hidden DML in there? Maybe check the debug log for events immediately before upsertProject is called. Commented Jul 29, 2016 at 1:25
  • @Eric no, there are no email alerts on any of the objects
    – jonnybro
    Commented Jul 29, 2016 at 1:26
  • @DanielBallinger upsertProject is already included under "HTTP Callout Method" above. No other DML is occurring. I've added the mock class to the post as well for you, it's a pretty standard implementation.
    – jonnybro
    Commented Jul 29, 2016 at 1:29
  • @jonnybro What API versions are you using? Long ago in the dark days of 2013 there were oddities with the order that tests cases ran in. The other unknown is what Replicon.getProjectById(p.Id) is doing. Maybe you could just manually create a mock Replicon.Project for the purpose to exercising the API call. Commented Jul 29, 2016 at 2:04

3 Answers 3

3

I ran into something similar to what you're describing here. I had all data setup in a testSetup method and committed no DML's in between Test.startTest() and Test.stopTest(), just querying a record and passing that to the data to a web service.

I also confirmed that there were no fresh DML statements by adding a Limits.getDMLStatements() right before the call out, it returned 0. I have scoured the boards and this is the best example that's similar to the behavior I have run into.

I'm certain it's a bug on the Salesforce side, and unfortunately those can take a while to resolve and I need to have this deployed! I came up with a workaround, and I wanted to share it since it may save you (if you're still having the issue) or someone else a lot of misery!

In my mock class that I was calling during the test, I had my sample JSON responses defined as static strings. So I did a tweaked version of a response built in to the call out:

//This is one of the callout functions that I'm using to hit a REST API
global HttpResponse getAllTemplates() {

    HttpRequest req = setupBaseRequest(null);
    req.setEndpoint(baseEndPoint+'/templates');
    req.setMethod('GET');

    HttpResponse res;
    if (Test.isRunningTest()) {
        res = new HttpResponse();
        res.setBody(MyMockClass.templateExampleJSON);
        res.setStatusCode(200);
        res.setStatus('OK');
    }
    if (!Test.isRunningTest()) res = new Http().send(req);
    return res;
}

Here is my mock class (example only):

@isTest
global class MyMockClass implements HttpCalloutMock {
    global static String templateExampleJSON = '{"templateName":"TestName","TemplateId":"12345"}';

    global HTTPResponse respond(HTTPRequest req) {
    HttpResponse res = new HttpResponse();
    res.setStatusCode(200);
    res.setStatus('OK');

    if (req.getEndPoint().contains('/templates')) {
        res.setBody(templateExampleJSON);
    } else {
        res.setStatusCode(400);
        res.setStatus('Bad Request');
    }

    return res;
}

Doing it this way allows my test class to fire correctly. It also allows the actual response to go out when it's not testing. I feel like it's a little bit of a hack; however, it accomplishes my end goal of testing the functionality.

1

I had a very similar issue but I was able to fix it by adding Test.startTest and stopTest before and after the web service call, as follows:

Test.startTest();

    Test.setMock(WebServiceMock.class, new SRM_MetadataApiMocks.DescribeMetadataResponse());

    List<String> metadataTypes = SRM_SelectMetadataController.getMetadataTypes('callout:fakeNamedCredential');

    Test.stopTest();
1

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

You must log in to answer this question.

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