32

So I have a method that takes as a parameter an Id of a object and uses it to make an external callout. Unfortunately I can't seem to figure out any way to test it.

@isTest static void sometimes_salesforces_sucks() {
   My_Object__c foo = new My_Object__c(Name='test');
   insert foo;
   MyClient client = new MyClient();
   MyMockResponseGenerator mockResponse = new MyMockResponseGenerator();
   Test.setMock(HttpCalloutMock.class,mockResponse);

   Test.startTest();
   client.bar(foo.Id);
   Test.stopTest();
}

This question says that using start/stop test should work, but it doesn't work for me

13
  • 2
    Can you post your code? When is your callout initiated in code?
    – greenstork
    Commented Aug 8, 2014 at 22:02
  • 1
    This pattern works for me. Two things you could check: 1) that there are no callouts made as a side effect of any code before Test.startTest; 2) that there are no updates made before the callout within the bar method. (One example of a "hidden" update would be a class that automatically inserts uninitialised custom settings when a setting value is referenced for the first time.)
    – Keith C
    Commented Aug 8, 2014 at 22:52
  • @greenstork sorry i thought that was obvious. client.bar();
    – NSjonas
    Commented Aug 9, 2014 at 0:14
  • 2
    Try to use @isTest(SeeAllData=true), and use an existing foo id (remove the insert of foo from test), if that solve the problem, then the insertion of foo doing something else (maybe a trigger). By the way I have objects that were inserted before the test (like your foo) which didn't had any trigger or something that related to the call out, but yet by insert them the uncommited work pending error produced, if that will be the case the only solution I have found is to use the old method of bypass call out in test --> Test.isRunningtest() (do something else when testing)
    – GoldenAxe
    Commented Aug 9, 2014 at 18:34
  • 1
    I ended up just bypassing the callout. I'm testing the callout directly in another place so i don't lose any code coverage. just feels hacky...
    – NSjonas
    Commented Aug 12, 2014 at 20:07

7 Answers 7

22

You can't do a web service callout after a DML operation in the same transaction. try this code:-

@isTest static void sometimes_salesforces_sucks() {
   My_Object__c foo = new My_Object__c(Name='test');
   insert foo;
   MyClient client = new MyClient();
   Test.startTest();
   Test.setMock(HttpCalloutMock.class,new MyMockResponseGenerator());
   client.bar(foo.Id);
   Test.stopTest();
}

You cannot perform a DML operation prior to a callout. All the DML operations should be invoked only after you are done with callouts.So, make a webservice callout first and then save the request.

If you are making multiple callouts, then save all the requests in a list or map and post callouts you save them.

Basically the following scenario will work :-

query callout query callout insert

callout callout callout insert or update

But the following scenario will fail :-

callout insert callout <---- fails="" here="" p=""> Possible workaround :-

splits the transaction into two separate Ajax processes. The first inserts the record and the second performs the callout and is able to update the newly inserted record.

4
  • 3
    You comment and your code are a bit conflicting. You are correct but you need to add why your code works so the OP can understand it.
    – Eric
    Commented Sep 18, 2015 at 6:38
  • I'm having the same problem. One way it doesn't fail - comment out static initialization section, where I make a bunch of insert/update DMLs. Also, my test is the same as @NSjonas has. Surrounded by Test.startTest()/Test.endTest(). Commented Jun 22, 2016 at 17:51
  • This isn't always true, I've got loads of tests that make callouts with a mocked response, and those can happen after I've inserted test data. Commented May 3, 2018 at 3:22
  • Does this work for you? Since this doesn't work for me. There is definitely some bug in Salesforce which is still not fixed, so these workaround do not work.
    – Patlatus
    Commented Jul 4, 2019 at 15:18
8

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

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

Update: This is the known issue https://success.salesforce.com/issues_view?id=a1p3A000000ATF1QAO, we need subscribe to it and wait for a fix or use some workaround meanwhile if applicable

5
  • I have the same issue. Unit test works ok if I don't insert a user record in test data. If I insert a user then callout in a queueable doesn't work - throws the same 'you have uncommitted work pending' error. Moving the data setup to a @TestSetup method doesn't fix the issue, couldn't find any workaround and will have to find a different way to test the code... :/
    – sskular
    Commented Sep 25, 2019 at 19:53
  • @sskular I think you are affected by this known issue success.salesforce.com/issues_view?id=a1p3A000000ATF1QAO subscribe to it and wait for a fix or use some workaround meanwhile
    – Patlatus
    Commented Sep 27, 2019 at 13:47
  • 2
    If my customer didn't want salesforce I would never use it. This has to be the WORST framework for building production grade applications I have EVER run across! How on earth are they such a large and successful company with such BAD software? Commented Oct 6, 2020 at 20:56
  • @SoftwareSavant I feel your pain, bro.
    – Patlatus
    Commented Oct 9, 2020 at 9:32
  • I still have this issue even after the Spring '21 release. Commented Feb 18, 2021 at 17:29
4

After playing around with it a bit, I found the following worked for me:

  ALL DML and *queries* (I found this necessary, as well) goes here

  Test.startTest(); // before Test.setMock
  Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());
  client.bar(foo.Id)
  Test.stopTest();
0
2

At the moment of writing (June 2019), unit tests are not always working as expected when it comes to libraries/folders/files. Code running fine 'live' may throw exceptions like that in unit tests.

What is probably going on, my guess, is that deep under water storage is not fully in the database but also partly on a file system. Then, a lot of magic happens with ContentXxxxx objects to make it appear as if we deal with standard SObjects. Think of creation of a ContentDocument on insert of a ContentVersion, or creation of a ContentFolderMember to the root folder of a library if you create a new ContentDocumentLink.

Creating those 4 objects does not work in a unit test though.

Things you try in the unit tests (which have to be rolled back after the test) may not behave the same as in actual execution. It may save you a lot of time to isolate the ContentXxxxx functionality, and leave it in the 25% untested code Salesforce allows you. The test outcome you get is not assured to be representative of the production behavior at the moment yet. Just make sure it works fine by other means.

It may sound a bit negative, but then I just spend two days too trying to work around the issues. :-)

0
0

I got the same issue. Fixed it by creating Mock Data within Test.StartTest and Test.StopTest. Note: Test.LoadData is inserting test data from Static Resource CSV.

    List<Account> accList=Test.loadData(Account.sObjectType,'challengeAccounts');AccountListController controller = new AccountListController();     
    Test.startTest();
    // Set mock callout class
    Test.setMock( HttpCalloutMock.class, new OpportunityMockHttpResponseGenerator() ); 

    controller.GetOpportunityList();
    Test.stopTest();
0
0

After much searching, I think I found an interesting solution that might have a general application. At the end of the day, this error is triggered by the fact that you have a DML and a callout in the same transaction. You can't force a commit (ie: you can't arbitrarily decide when the transaction ends), but you can make sure you generate your Test Data in a separate transaction. Here's how: use @testSetup

Use test setup methods (methods that are annotated with @testSetup) to create test records once and then access them in every test method in the test class

By creating all your test Data in @testSetup (or by loading static resources with loadData), you are keeping them in a separate transaction, and then you can do all the callouts your heart desires.

https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing_testsetup_using.htm

0
0

This answer won't offer anything new by way of an explanation for why this is happening, but it may offer another solution for those who are struggling with the issue.

I started getting this error on a test method that was previously passing. The interesting thing was -- I hadn't made any changes to that method whatsoever. I was making what I thought were unrelated changes, implementing a @TestSetup method in the class, and making other methods in the class more efficient.

I ended up splitting my method out into a separate test class, and now it's working fine.

You must log in to answer this question.

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