3

I have a batch apex class that makes a callout at the beginning of each batch. I'm pretty certain that the callout error I'm getting isn't related to the order of DML statements and callouts within the method I'm testing.

However, the method I'm testing requires that there be certain SF records in place in the system for it to work. Therefore, in order to test the method, I need to 'generate' some test data within my test method. I figured if I put the code that creates and inserts records before the test.startTest(); line, it wouldn't trigger the error about making callouts with work pending. Nope, still got the error!

So am I right about the reason for my error? If I start a TestMethod by inserting an account record, then fire up test.startTest();, then execute the method that has a callout in it, will that give me an error? If so, how do I get around the error, since I need those records to exist before I run my method?

I should note that its not important that the records be inserted into the system in the same execution as the code i'm testing. The idea is that those records would already be there.


Edit in response to SFDCFox's answer:

Well, I still can't get it to work. I put all my test data generation methods (with the DML commands) before the test.StartTest() block, then put my batchable code I was testing in the test block. Still got the System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out error


Second Edit: the problem, clarified, and the solution I used

So by now I've confirmed that SF has a legitimate issue with testing callouts that are done within batch apex. Their documentation on the issue only covers tests that involve callouts made separate from batch apex.

Here's the basic outline of my test method:

  1. Create and insert an account record.
  2. Set the mock callout class, and in that process pass the inserted Account ID to the class so the callout's mock response can include the ID.
  3. Within the test.StartTest() and test.StopTest(), I execute the batch apex, which contains the actual callout within its execute portion.

The error is due to inserting the account BEFORE making the callout. Using starttest()/stoptest() as suggested by SFDCfox doesn't seem to solve the issue, nor does the other linked solution on this thread that suggests testing the individual components of the batch process, rather than simply running a batchexecute. This is with the API # of all my classes updated to 30.0.

the solution I used

As linked in PJC's comment below. Essentially, rather than using the testMock class to create fake responses, I created an if clause within the batch apex itself where the callout was made. Before making the callout I check if the callout is being made during a test or not. If it's made during a test, rather than making the actual callout I simply assign the desired test response to the response variable.

11
  • 1
    see cropredysfdc.com/2014/06/18/… for batchable with callouts
    – cropredy
    Sep 12, 2014 at 22:19
  • 1
    Check the answer on this post: salesforce.stackexchange.com/questions/11606/…
    – pjcarly
    Sep 13, 2014 at 8:22
  • @crop1645 Yup, that's one of the first I looked at. Unfortunately, replacing the standard batch execute call with the individual batch methods didn't solve the problem for me!
    – smohyee
    Sep 17, 2014 at 16:16
  • 1
    The issue is not with inserting data. In fact, you can perform NO DML, and then attempt to test a batch making a callout using the mock response and you will still get the uncommitted work pending. There is / has been an issue with mock implementation and executing batches. The way around it is to not execute the batch using database.executebacth, rather, call the execute method directly
    – Eric
    Sep 17, 2014 at 17:26
  • 1
    @smohyee - That is correct. Either you are not implementing that correctly or there is something in your code causing it cause it works as outlined in that link. Unless something has changed in the API version in your sandbox
    – Eric
    Sep 17, 2014 at 19:32

2 Answers 2

2

See Performing DML Operations and Mock Callouts for more information, but in essence, you need to insert data before Test.startTest, and callout after Test.startTest. Also, this doesn't work with older API versions, so make sure your test methods and classes are running the latest version, if possible. Also note that calling future methods, batches, etc, cause uncommitted work errors, so in some cases you need to call the callout after Test.stopTest, also mentioned in the document.

Test.startTest();
MyClass.asyncCall();
Test.stopTest();

Test.setMock(..); // Takes two arguments
MyClass.mockCallout();

--- or ---

MyClass.asyncCall();

Test.startTest();
Test.setMock(..); // Takes two arguments
MyClass.mockCallout();
Test.stopTest();

Also see Testing HTTP Callouts for more information about testing in general (e.g. the mock callout interfaces, etc).

4
  • 2
    i almost unquestioningly trust everything you say but with respect to inserting test records with batchable that does callouts, the only thing I could ever get to work is here: cropredysfdc.com/2014/06/18/… . Are you saying there is a batchable/callout scenario alternative by invoking the callout after Test.stoptest()?
    – cropredy
    Aug 29, 2014 at 22:26
  • @sfdcfox here's my issue: my mockcallout generator needs to include an SF Account ID in its JSON response, and that ID needs to match an existing account record in SF. That means that for my test, I need to insert a new account, then pass that account's ID to the mock callout generator class. THEN, when the callout is made, the ID will be included in the response so the code can be tested. Can you think of any solution that would work within this parameter?
    – smohyee
    Sep 2, 2014 at 17:40
  • The second argument of Test.setMock is an object of a class that implements the respond() method. So, you could do the following: Test.setMock(HttpcalloutMock.class, new yourMockResponseClass(theAccountId)); once you have inserted the account
    – cropredy
    Sep 12, 2014 at 22:19
  • @crop1645 That is precisely what I had done, but that wasn't the issue I was attempting to outline in the previous comment. My issue was due to my code passing the inserted Account to my mock callout, since that required that I insert the account via DML first, then pass it to the callout mock, THEN make the actual callout, which causes the callout exception error.
    – smohyee
    Sep 17, 2014 at 16:20
2

smohyee

In response to your last comment, look at the following code which does not get a callout exception

The class being tested (greatly simplified):

public with sharing class Foo {
 public String doSomething() {
    Http        h       = new Http();
    HttpRequest hRqst   = new HttpRequest();
    hRqst.setEndpoint('http://my.foo.bar/service');
    hRqst.setMethod('GET');
    hRqst.setTimeout(12000);    
    hRqst.setHeader('Content-Type', 'application/json');    
    HttpResponse hResp =  h.send(hRqst);                    // make the callout.
    return hResp.getBody();                                 // simplified, ignores error checking
 }
}

The testclass with mock callout response and account test data

@isTest
private with sharing class TestFoo {

public class MockCallout implements HttpCalloutMock {       
    private ID  aId;

    public MockCallout(ID aId) {
        this.aId = aId;
    }

    public HTTPResponse respond(HTTPRequest req) {
        HttpResponse res = new HttpResponse();
        // add inserted obj's id to mock response
        res.setBody('{"id" : ' + this.aId + ', "nextAttr" : "bar"}'); 
        res.setStatusCode(200);
        return res;
    }
}

@IsTest
private static void testMyFoo () {
    //  Insert test data before Test.startTest()
    Account a = new Account(name = 'fooAccount');
    insert a;

    Test.startTest();
        //  Establish the mock response, note: pass in ID of inserted Account
        Test.setMock(HttpCalloutMock.class,new TestFoo.MockCallout(a.id));
        Foo foo = new Foo();    // the class being tested
        System.debug(LoggingLevel.INFO,'foo.doSomething returns: ' + foo.doSomething());
    Test.stopTest();
}
}

The debug output clearly shows the mock response including information from the inserted test objects

USER_DEBUG|[30]|INFO|foo.doSomething returns: {"id" : 001J000001RjovnIAB, "nextAttr" : "bar"}
4
  • I agree that your code example should work, and the relevant SF documentation agrees with you (salesforce.com/us/developer/docs/apexcode/Content/…). However, one of the key factors in this issue seems to be the fact that my callout is happening from Batch Apex. I'm betting that if you change your class being tested to a Batch Class, and put the callout code within the execute portion, you'll get the same error as me!
    – smohyee
    Sep 17, 2014 at 19:21
  • smohyee -- see my blogpost cropredysfdc.com/2014/06/18/… on how to set up test data and use a batch class that does a callout (in my case, the callout is done in the batch start() method. ) You have to test the batch's start(), execute(), and finish() methods individually. You can't invoke executeBatch() in the testmethod
    – cropredy
    Sep 17, 2014 at 20:58
  • You're gonna LOL once you read the comments thread under my original question above... I've already tried the advice on your page from when you posted that here on Aug 29th! As I stated in the edit to my original post, I still got the callout exception error even when calling the individual batch methods in my test rather than doing a batchexecute call. As I promised Eric, I'm going to try your solution with a very simple batch class and see if I still can't get it to work, and if I can't then I'll post that code for review.
    – smohyee
    Sep 17, 2014 at 21:09
  • 1
    yes, I saw that and felt you must be doing something wrong or have a much more complex case than my blogpost as I have this use case developed and deployed in PROD
    – cropredy
    Sep 17, 2014 at 21:17

You must log in to answer this question.

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