Unit testing a callout on batch class, I got some differences when using different standard objects

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).