2

I am facing weird behavior of test running. I have two sandboxes and production. One sandbox is where development is done, second to test. I developed class and deployed it to second sandbox, run all tests and all are passed. This second sandbox is refreshed today, so no difference between it and production. When I tried to deploy codebase to production, I am having CalloutException, There is no DML before callout

System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out Stack Trace: Class.APICalloutHelper.getResponse: line 13, column 1 Class.Product1APIDaily.doCakeAPIDaily: line 4, column 1 Class.ExternalCakeReports.doCakeAPIDaily: line 14, column 1

Such exception isn't thrown neither on first, or second sandboxes. Here are source test-class

@isTest
private class ExternalCakeReportsTest {

    @isTest
    static void testSchedule(){
        TestData setupData = new TestData();
        System.runAs(setupData.testUser) {
            setupData.initCutomSettings();
            String cronExp = '0 0 1 * * ?';
            Test.startTest();
                Test.setMock(HttpCalloutMock.class, new ExternalCakeReportsMock());
                String actualJobId = System.schedule('Test ExternalCakeReports Test',cronExp, new ExternalCakeReports());
            Test.stopTest();
            CronTrigger actual = [SELECT Id, CronExpression, TimesTriggered, NextFireTime FROM CronTrigger WHERE id = :actualJobId];
            System.assertEquals(cronExp, actual.CronExpression, 'Incorrect Crom Expression');
        }
    }

    public class ExternalCakeReportsMock implements HttpCalloutMock {
        public HttpResponse respond(HttpRequest req) {
            HTTPResponse res = new HTTPResponse();
            res.setHeader('Content-Type', 'text/json');
            res.setBody('{"data":[], "error":"Test error"}');
            res.setStatusCode(200);
            return res;
        }
    }

    public class TestData {
        public User testUser {get; set;}

        public TestData(){
            testUser = TestUserData.getUserForTest()
                                    .setProfile(ProfileUtilities.ProfileType.INTEGRATION)
                                    .save()
                                    .getRecord();
        }

        public void initCutomSettings(){
            insert new CakeAPI__c(
                Name='Product1',
                Endpoint__c = 'http://oberegovskyi.com',
                APIKey__c = 'sdfdsfDSFFdfdf'
                );
            insert new CakeAPI__c(
                Name='Product2',
                Endpoint__c = 'http://oberegovskyi.com',
                APIKey__c = 'sdfdsfDSFFdfdfdfdf'
                );
        }
    }
}

and class

global with sharing class ExternalCakeReports implements Schedulable, Database.AllowsCallouts {

    global void execute (SchedulableContext ctx) {
        doCakeAPIDaily();
        doCakeAPIMonthly();
        doAdvertiserReport();
        doGetProduct1AccountStatuses();
        doGetProduct2AccountStatuses();
    }

    @future(callout=true)
    private static void doCakeAPIDaily() {
        Product1APIDaily daily = new Product1APIDaily();
        daily.doCakeAPIDaily();
    }

    @future(callout=true)
    private static void doCakeAPIMonthly() {
        if(Date.today().day() == 1){
            Product1APIMonthly monthly = new Product1APIMonthly();
            monthly.doCakeAPIMonthly();
            SalesGoalsUpdater goalsUpdater = new SalesGoalsUpdater();
            goalsUpdater.doUpdate();
        }
    }

    @future(callout=true)
    private static void doAdvertiserReport(){
        Date dateYesterday = Date.today() - 1;
        String advertiserId= '0';
        CDAdvertiserReport adv = new CDAdvertiserReport(dateYesterday, dateYesterday, advertiserId);
        adv.callAdvertiserReport();
    }

    @future(callout=true)
    private static void doGetProduct1AccountStatuses() {
        CakeStatus status = new CakeStatus(CsApiTypes.Product.Product1);
        status.updateStatuses();
    }

    @future(callout=true)
    private static void doGetProduct2AccountStatuses() {
        CakeStatus status = new CakeStatus(CsApiTypes.Product.Product2);
        status.updateStatuses();
    }
}

1 Answer 1

2

Honestly, I'm a bit surprised it is passing in the sandboxes as it is.

The execute of ExternalCakeReports queues up five asynchronous future methods that all appear to make callouts of their own.

When Test.stopTest(); fires it is first going to execute the scheduled job, and then make those five future methods synchronous as well. They are no longer executing in their own isolated transactions, and the order they run in isn't defined (as would be the case when they run outside a test case).

I'd suggest you break the current test down into several separate test cases. Have one that ensures the scheduled class executes as expected, but doesn't call the future methods. Then several other tests for each of the individual future callout methods.

You will likely need to use a mixture of Test.isRunningTest() and the @testVisible attribute to get the required isolation.


If you really want to know why it is failing with the deployment to production but not in the sandboxes, check the debug logs. It's possible it is being stricter with production, or that it is interacting differently with some existing data (such as a custom setting). Either way, there isn't currently a supported way to test multiple future method callouts reliably in on test method.

You must log in to answer this question.

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