36

We have a callout to an external webservice that requires retrieving some data from the database via SOQL and then issuing an HTTP GET request, and performing various logic based upon the response. Trying to make our unit tests as robust as possible, we try to always create test data from within the unit tests. However, it seems that creating test data within a unit test, and then trying to do a callout, even when using HttpCalloutMock and Test.startTest() and Test.stopTest() will still throw an exception.

While we could use some hard-coded values in the callout unit-test, we'd really prefer not to, as we'd like to have our tests cover as much of the end-to-end as possible, including retrieving the data from the database (which won't exist unless we create it from within the unit test).

We could separate the logic out into multiple unit tests, one for testing the retrieving of the data, and another for testing the callout mechanism, but I'd worry there may be logic branches that rely on not only data in the database, but also certain HttpResponses that may be hard to traverse without muddying-up the actual non-test code.

Here is a very much simplified example that shows the problem.

Callout Class:

    public class CalloutClass {
        public static HttpResponse getInfoFromExternalService(Id myCaseId) {
            Case myCase = [SELECT Id, Account.Integration_Key__c FROM Case WHERE Id = :myCaseId LIMIT 1]; 
            HttpRequest req = new HttpRequest();
            req.setEndpoint('http://api.salesforce.com/foo/bar?id=' + myCase.Account.Integration_Key__c);
            req.setMethod('GET');
            Http h = new Http();
            HttpResponse res = h.send(req);
            return res;
        }
    }

And the HttpCalloutMock Class:

    @isTest
    global class ExampleCalloutMock implements HttpCalloutMock{
      global HttpResponse respond(HTTPRequest req){
        HttpResponse res = new HttpResponse();
        res.setStatus('OK');
        res.setStatusCode(200);
        res.setBody('GREAT SCOTT');
        return res;
      }
    }

And the TestClass:

    @isTest(seeAllData=False)
    private class ExampleCallout_Test{
      
      static Case getTestCase(){
        Case myCase = new Case(Subject = 'Test');
        insert myCase;
        return myCase;
      }
    
      static testMethod void shouldBeAbleToGetData(){
        Case myCase = getTestCase();
        Test.startTest();
        Test.setMock(HttpCalloutMock.class, new ExampleCalloutMock());
        HttpResponse res = CalloutClass.getInfoFromExternalService(myCase.Id);
        Test.stopTest();
      }
    }

And here is the result:

15:30:18.239 (5239722000)|EXCEPTION_THROWN|[45]|System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out

I'm curious if perhaps I'm missing something, if this is a bug, or is it by design to not allow setting up test data when performing a mocked callout? If there are any creative solutions or workarounds I'd love to hear them as well. :)

4
  • 4
    Cross-posting an answer I gave on a blog comment: "I've confirmed that you cannot use mock callouts with pending DML. The problem is that we would want to catch the erroneous case where non-test code was doing DML before a callout. Filing a bug to see if we can maybe allow DML and callouts where there is a Test.startTest() between them."
    – metadaddy
    Commented Oct 21, 2012 at 6:38
  • @metadaddy Any follow up on this? Commented Nov 7, 2012 at 2:47
  • 2
    @DanielBallinger Bug (well, actually a 'user story') filed. It's on the Apex team's backlog. No scheduled build yet.
    – metadaddy
    Commented Nov 8, 2012 at 18:57
  • @DanielBallinger I'm pretty sure this is fixed in Spring 13. I solved this error using Test.startTest() in a CS3 Sandbox(Spring 13) before I saw this bug. I then refreshed my Sandbox and it moved to CS1 (Winter 13) whereupon my previously passing tests began to fail with this error. I then came here and found out why, can't wait until Spring 13, time to workaround it...
    – mjgallag
    Commented Jan 17, 2013 at 21:04

6 Answers 6

16

One option, although not ideal, would be to use @isTest(SeeAllData=true) rather than creating the Case in your test case.

You can't make callouts once you have made changes to the database (in this case the Case insertion proceeds the callout). It seems like this applies to test case setup as well even though it occurs prior to the Test.startTest().

Another option would be to make the mock callout in an @future method and invoke this future method within the Test.startTest() and Test.stopTest(). Updated - This doesn't resolve the issue (at least if the future method is defined in a test class). The CalloutException still occurs with the future method appearing at the bottom of the stacktrace.

To save others the effort, the test data mixed mode DML solution of wrapping the setup code in System.runAs(user) { ... } doesn't help either. Thought it was worth a long shot, but it didn't help.

For reference, the forum post Test method custom setting and callout - Uncommitted work pending covers much the same question. They ultimately reference another post by Bob Buzzard with the suggested resolutions:

You can't make callouts, HTTP or otherwise, once you have made changes to the database. You either need to commit the transaction, make the callout prior to any database changes or move your callout to an @future method.

Ideas: Allow loading test data prior to callout testing is worth promoting.

5
  • Yeah, we're trying to avoid the seeAllData=true so we can run this test in a clean-org. I really like the idea of making the mock callout @future, I'll give that a whirl.
    – Mikey
    Commented Oct 18, 2012 at 23:36
  • I wonder if you can do @ future in an @ isTest context. Would be good to know. Commented Oct 18, 2012 at 23:52
  • @techtrekker My understanding was the Test.startTest() and stopTest() are used to inline Asynchronous calls such as @future for testing. This is how I've always used them. Or did you mean something else, such as using the @future attribute on a method within the @isTest class? In which case, I don't know either! Commented Oct 19, 2012 at 0:20
  • Yeah I meant the latter :) Commented Oct 19, 2012 at 0:32
  • Funny what comes around. Thought I'd try testing a WebServiceCallout.invoke using WebServiceMock and ran straight into the CalloutException issue. Commented Nov 7, 2012 at 2:30
16

The workaround I used is to manually keep track of the mock response, and call the respond method on that instead of Http.send when running a test.

In your callout class it would look something like:

public class CalloutClass {
    public static HttpCalloutMock mock = null;
    public static HttpResponse getInfoFromExternalService(Id myCaseId) {
        Case myCase = [SELECT Id, Account.Integration_Key__c FROM Case WHERE Id = :myCaseId LIMIT 1]; 
        HttpRequest req = new HttpRequest();
        req.setEndpoint('http://api.salesforce.com/foo/bar?id=' + myCase.Account.Integration_Key__c);
        req.setMethod('GET');
        if (Test.isRunningTest() && (mock!=null)) {
            return mock.respond(req);
        } else {
            Http h = new Http();
            return h.send(req);
        }
    }
}

and in the test you would replace the Test.setMock by

CalloutClass.mock = new ExampleCalloutMock();

It's somewhat intrusive, and would hide cases where there is real DML being done before a callout. But it least gets the test to work pending a better solution.

5
  • I am facing callout issue in test class -Methods defined as TestMethod do not support Web service callouts please help me for this scenario @Jelle van Geuns
    – SFDC
    Commented Oct 21, 2017 at 6:29
  • How to rectify?
    – SFDC
    Commented Oct 21, 2017 at 6:34
  • Hi @SFDC Please check this article: developer.salesforce.com/docs/atlas.en-us.apexcode.meta/… Commented Oct 21, 2017 at 6:34
  • I would suggest you do a little research of your own. For instance in developer.salesforce.com/docs/atlas.en-us.apexcode.meta/… you will see "By default, test methods don’t support HTTP callouts, so tests that perform callouts fail." Which answers your "why" question Commented Oct 21, 2017 at 7:34
  • @ Jelle van Geuns,ok thank you, now what i have to replace in test class
    – SFDC
    Commented Oct 21, 2017 at 7:46
4

This is especially a problem if you have a trigger that calls a future method which makes the callout. The insert needed to fire the trigger will cause the unit test to fail.

2

I've just come across this defect as well. My solution was to modify the callout method to take parameters for the various fields I need rather than rely on sobject data. I also modified the method to be public so that I could call it from the test class. Although not ideal, it allows me to get around the uncommitted exception.

0

The exception is occurring because you are executing a DML operation (insert of Case) before making the mock http call.

In a non-test implementation, to get around this, the http callout would be done in a @future method, so it happens asynchronously, not in the same context.

In this case, since you are mocking the http response, you don't really seem to need to insert the case before making the http call out.

Try getting rid of the getTestCase() method which is creating a test. Instead in your CalloutClass just add an exclusion to the query for Test.isRunningTest(). You're returning a mock response in any case.

3
  • Not inserting the case does in fact prevent the exception from being thrown, however it makes our current unit test less beneficial, as this simplified example leaves out additional querying that needs to be done from the DB, that ideally we would setup with test data.
    – Mikey
    Commented Oct 18, 2012 at 23:35
  • Agree. I would probably unit test the querying logic in a dedicated test method and only test the response in this mock http callout. Commented Oct 19, 2012 at 0:49
  • except if you do a future callout it becomes almost impossible to test as you will still get the pending work issue
    – NSjonas
    Commented Feb 20, 2016 at 0:31
0

I've been able to get around
(a) the 10 callout limit, and
(b) the problem of calling out then using DML in a loop.

If you can possibly use a Batch Apex class to process these, you can login if necessary in the start() method, make one callout each in the execute() method, and process anything further in the finish() method.

In order to make callouts, you'll need to add Database.AllowsCallouts after the Database.Batchable<> keyword in the class definition. In order to access a variable throughout each of the iterations from start, through execute, to finish (like an authToken), you'll need to add Database.Stateful as well.

Usually you can only instantiate a batchable job with a database query, but you can also create a CustomIterable and a CustomIterator to facilitate something different, like a list of custom Apex Classes or a list of SObjects that haven't been inserted yet.

You must log in to answer this question.

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