6

I have a class that is a batch job to upload files to Google Drive using the service's REST API. This particular class used to work only for Attachment objects since it was designed for Salesforce Classic. On Lightning Experience, however, the attachment content is stored on different tables (ContentVersion, to be more accurate). I can still get the content, file name, and other fields in a way that I can mimic the attachment behavior.

My problem is with the test class. The class inserts an account, then an attachment, and then using Test.startTest I call the batch class. The method that uses the Attachment object works fine. However, when running the other method, for ContentDocument, I get the following message right before the callout:

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

I'm having a difficult time to get to the root of this problem. I literally don't insert/update anything before the callout transaction (the same code runs for the first method, using attachments, after all).

Is there some difference for Content Documents? Some internal trigger, or something like that?

I also tried to use System.runAs() to create the objects first, and then make the callout outside this scope, with Test.startTest() but with no success.

Part of the test class is like this:

Test.startTest();
Test.setMock(HttpCalloutMock.class, new ClienteMock());
GoogleDriveIntegrationBatch batchJob = new GoogleDriveIntegrationBatch();
Database.executeBatch(batchJob, 1);
Test.stopTest();

The setup objects like account, attachment and the custom setting with mock credentials are created/inserted before the Test.startTest() invocation.

Edit

After using the Limits class right before the callout statement, I got the following:

11:51:08.93 (1637716497)|USER_DEBUG|[48]|INFO|DML Rows: 0
11:51:08.93 (1637799554)|USER_DEBUG|[49]|INFO|DML Statements: 0
4
  • There's a lot of back and forth here, so I moved the conversation to chat.
    – Adrian Larson
    Commented Apr 18, 2018 at 17:29
  • @AdrianLarson You can clean up the comments under my answer as well if you'd like ( not sure if they can get merged into that chat ) Commented Apr 18, 2018 at 18:45
  • Not the same chat. I can create a separate one if you like or just purge.
    – Adrian Larson
    Commented Apr 18, 2018 at 18:47
  • Up to you, I think I edited the relevant bits into the answer, so you might as well purge them Commented Apr 18, 2018 at 18:56

2 Answers 2

0

I found this question/answer when I ran into the same problem. I made a smaller repro, starting from batter.cord's, above. Salesforce support have confirmed that it is a bug and will be fixed, but there is no ETA for the fix. It's not currently on Known Issues, but I'll update if they add it. We have a bug number W-4947848

FYI, my repro follows. The insertContact() test passes, the insertContentVersion() test fails.

@IsTest
private class ContentVersionBugRepro {

    public class MyQueueable implements Queueable, Database.AllowsCallouts {
        public void execute(QueueableContext param1) {
            Http connection = new Http();
            HttpRequest request = new HttpRequest();
            request.setEndpoint(System.Url.getOrgDomainUrl().toExternalForm());
            connection.send(request);
        }
    }

    public class MyMock implements HttpCalloutMock {
        public HttpResponse respond(HttpRequest param1) {
            return new HttpResponse();
        }
    }

    @IsTest static void insertContact() {
        insert new Contact(LastName = 'Test');

        Test.setMock(HttpCalloutMock.class, new MyMock());
        Test.startTest();
        System.enqueueJob(new MyQueueable());
        Test.stopTest();
    }

    @IsTest static void insertContentVersion() {
        insert new ContentVersion(
                VersionData = Blob.valueOf('someData'),
                PathOnClient = 'someData.txt'
        );

        Test.setMock(HttpCalloutMock.class, new MyMock());
        Test.startTest();
        System.enqueueJob(new MyQueueable());
        Test.stopTest();
    }
}
3
  • Please do share the issue number. Commented Sep 24, 2018 at 10:13
  • @Aidan did Salesforce close that bug W-4947848? Can you remind them about it?
    – steals
    Commented Sep 17, 2020 at 9:20
  • Sorry, I never heard back about it and I don't have an inside line to get past support fobbing me off for weeks on end, so I can't really follow up.
    – Aidan
    Commented Sep 17, 2020 at 13:05
5

Working answer, heres the code I wrote to repo your exception:

Batch Class

public class SomeBatchClass implements Database.Batchable<sObject>, Database.AllowsCallouts {

    public List<sObject> start(Database.BatchableContext context) {
        return [
            SELECT Id FROM ContentVersion 
        ];
    }

    public void execute(Database.BatchableContext context, List<sObject> records) {
        HttpRequest accessTokenRequest = new HttpRequest();

        System.assertEquals(0, Limits.getDmlRows());
        System.assertEquals(0, Limits.getDmlStatements());

        new Http().send(accessTokenRequest).getBody();

        update records; // make sure we can run dml after callout 
    }

    public void finish(Database.BatchableContext context) {
        // do nothing .. 
    }

}

Batch Test Class

@isTest
public class SomeBatchTest {

    @testSetup
    private static void Setup() {
        ContentVersion documentVersion = new ContentVersion(
            Title='Tigers',
            PathOnClient='cute_tigers.jpg',
            VersionData=Blob.valueOf('tigers pic'),
            IsMajorVersion=true
        );

        insert documentVersion;
    }

    @isTest 
    private static void Test() {
        // Inserting ContentVersion here also causes Uncommitted Work Pending error.. 
        Test.startTest();

        Test.setMock(System.HttpCalloutMock.class, new SomeBatchMock());
        Database.executeBatch(new SomeBatchClass(), 1);

        Test.stopTest(); 
    }

}

Batch Mock Class

public class SomeBatchMock implements HttpCalloutMock {

    public HttpResponse Respond(HttpRequest request) {
        HttpResponse response = new HttpResponse(); 

        response.setBody('Dummy Body');

        return response; 
    }

}

It seems like inserting a ContentVersion anywhere in the test, and then trying to run Database.ExecuteBatch will cause a failure.

However, it seems like creating your own instance of the batch class and running the individual steps does not cause the same failure!

@isTest
public class SomeBatchTest {

    @testSetup
    private static void Setup() {
        ContentVersion documentVersion = new ContentVersion(
            Title='Tigers',
            PathOnClient='cute_tigers.jpg',
            VersionData=Blob.valueOf('tigers pic'),
            IsMajorVersion=true
        );

        insert documentVersion;
    }

    @isTest 
    private static void Test() {
        // Inserting ContentVersion here also causes Uncommitted Work Pending error.. 
        Test.startTest();

        Test.setMock(System.HttpCalloutMock.class, new SomeBatchMock());

        // Database.executeBatch(new SomeBatchClass());

        SomeBatchClass someBatch = new SomeBatchClass(); 

        List<ContentVersion> versions = someBatch.start(null);

        someBatch.execute(null, versions);

        someBatch.finish(null);

        Test.stopTest(); 
    }

}

Theres other options here as well, such as saving your contentVersions as JSON, and deserializing them in a test which only calls the execute method, while other tests check each part of the batch class separately. However, it seems like using Database.ExecuteBatch is out of the question.


Some more gotchas:

  1. Returning a fixed (created without a query) List<sObject> from the start method, if a ContentVersion is inserted in the test, will fail anyway.
  2. If the job type is a different type, meaning it won't interact directly with the ContentVersion records, it'll still fail if you insert an ContentVersion record
  3. If you try to run the same batch job more than once in a context, you'll get the same error, even if the first job was successful
  4. There are no dml rows, queries, statements, @future methods, or other callouts reported by Limits, but the job will fail anyway.
  5. Trying to call System.ScheduleBatch instead will also fail in the same manner as Database.ExecuteBatch.
  6. Running the same job by manually by calling the start, execute, and finish methods will work after inserting a ContentVersion. However, trying to execute the job twice in the same context will cause a failure (as there are dml rows from that context).
  7. Both System.ScheduleBatch and Database.ExecuteBatch will return valid job ids before failing.

I'm not 100% sure of the cause, but theres clearly something happening behind the scenes when you insert a ContentVersion (probably the creation of relevant objects, such as ContentDocument's) which is causing the failure of these methods. This feels a lot like a bug, and I'd go as far as to contact support if I were you (I'd do it for you if I had better support options).

6
  • This does seems like a bug though, correct? It makes little sense to me that an attachment can be created, but a content document can not. I wonder if it is worth it to reach someone on Twitter about this or log a case... Commented Apr 18, 2018 at 17:52
  • 1
    Definitely seems like it, something isn't working right behind the scenes. Both Database.ExecuteBatch and System.ScheduleBatch return a valid job id (meaning the job queue can fit them), but then any callouts executed in this context fail. Even returning values directly from the start method, without any queries in the test or the batch class will cause this error if run with either of those methods. Run the same methods yourself, in the same context, and it works without issue. Commented Apr 18, 2018 at 17:57
  • 1
    .. This actually fails when working with Attachments as well, if you replace all the references in the job & test with Attachments, you get the same error pattern, which doesn't line up with your original post.. Honestly I have no idea what the root of this error is, but I think the solution on how to cover your test is clear from my answer. Commented Apr 18, 2018 at 18:14
  • Interesting, the test method works just fine with the Attachment object. The method that is failing is the one with the Content Document. It is the same logic for both objects, but I separate the test on two distinct methods. Commented Apr 18, 2018 at 18:16
  • 1
    Looking at the log I think it was failing after trying to execute the second batch job, I think that has something to do with it - removing (or moving them out of the Test.Start/Stop) the extra execute/start/finish methods I was using for testing makes the error go away. Seems like trying to execute the batch more than once in a single context will also cause this error. If you insert a ContentDocument in the @testSetup method, it'll still cause the same failure - even if the job is trying to work off attachments ! I'll make one last edit, but we should probably wrap this up.. Commented Apr 18, 2018 at 18:33

You must log in to answer this question.

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