[SalesForce] Uncommitted work pending in unit test with trigger and queueable callout

I have an after insert trigger that invokes queueable Apex which makes a callout. The trigger and callout work fine in the UI and I don't get any errors in normal operation.

However, my unit test keeps failing with the "uncommitted work pending" error despite all my attempts to separate the callout. I've used a testVisible variable to disable the callout from the trigger so that I can invoke the queueable job explicitly in a startTest/stopTest block after the DML that fires the trigger.

I've pored over the callout class to make sure it's not doing any DML beforehand. Anyway, if it was doing DML, I should get the uncommitted work pending errors in the UI, right? I've tried API version 37 and version 41 but the unit test still fails.

I've had a case open with Salesforce premier support for 6 weeks now but haven't made any progress. Any suggestions?

The unit test:

@isTest
static void testCallout() {
    // Disable the callout while setting up data.
    CCx_EHR_CMCT_Services.isQueueSendEnabled = false;

    // Set up the data and execute the DML.
    / ...
    insert act;

    // Set up the http mock
    // ...

    // Now enable and execute the callout
    Test.startTest();
    CCx_EHR_CMCT_Services.isQueueSendEnabled = true;
    Test.setMock(HttpCalloutMock.class, mock);
    System.enqueueJob(new QueueSendToRedox(new List<ActivityHL__c>{act}, desk)));
    Test.stopTest();

}

Trigger:

trigger HL_ActivityHL_c on ActivityHL__c (before insert, before update, after insert) {
    if (Trigger.isAfter) {
        if (Trigger.isInsert) {
            CCx_ClientDataDomain.sendEHRClinicalComm(Trigger.new);
        }
    }
}

Trigger handler class:

public without sharing class CCx_ClientDataDomain {
    public static void sendEHRClinicalComm(List<ActivityHL__c> triggerNew) {
    // do some processing here.
    // ...
    // then queue the callout job.
    System.enqueueJob(new CCx_EHR_CMCT_Services.QueueSendToRedox(
            deskActivities.get((Id) desk.getDeskId()), desk));
    }
}

Queueable job class with callout method:

public with sharing class CCx_EHR_CMCT_Services {
    // isQueueSendEnabled is an attempted workaround for unit tests
    @testVisible
    private static Boolean isQueueSendEnabled = true;

    public class QueueSendToRedox implements Queueable, Database.AllowsCallouts {
        private List<ActivityHL__c> activities;
        private CCx_DeskHierarchySelector.Desk desk;
        private Boolean isEnabled;

        public QueueSendToRedox(List<ActivityHL__c> activities, CCx_DeskHierarchySelector.Desk desk) {
            this.activities = activities;
            this.desk = desk;
            this.isEnabled = isQueueSendEnabled;
        }

        public void execute (QueueableContext context) {
            if(this.isEnabled) sendClinicalCommToRedox(activities, desk);
        }
    }

    public static void sendClinicalCommToRedox(List<ActivityHL__c> activities, CCx_DeskHierarchySelector.Desk desk) {
        // Set up data for the callout.  No DML actions are executed before the callout.
        HttpRequest req = buildRequest();
        // ...

        // Callout
        Http http = new Http();
        HttpResponse resp = http.send(req);
    }
}

Best Answer

You'll need to test the method directly, and test the Queueable separately. The easiest way to do so is a static flag.

public class MyQueueable implements Queueable
    @TestVisible static Boolean makeCallout = true;
    public void execute(QueueableContext context)
    {
        if (makeCallout) performCallout();
    }
    public static void performCallout()
    {
        // make callout here
    }
}

Testing the job:

Test.startTest();
MyQueueable.makeCallout = false;
// enqueue job
Test.stopTest();

Then test your method which actually makes the callout separately:

Test.startTest();
MyQueueable.performCallout();
Test.stopTest();

The reason you get this error is that when you enqueue the job, that counts as a DML Operation. Then when you call Test.stopTest(), it runs synchronously, making a callout within the same transaction.

Related Topic