6

I stumbled upon strange behavior when executing tests on deployment to production organization. In the test, I first insert a User object, some other objects and then as the last one, a CampaignMember object. Inserting CampaignMember triggers after insert event, which makes a callout to external service through queueable class.

When I do not create a user, everything works fine. I can create as many objects as I want and then insert CampaignMember, and callout is made fine. However, if I try to insert User object as well, I get a CalloutException:

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

This only happens on deployment to production. If I try to deploy to any sandbox running all tests on deployment, I don't get the exception, whole deployment works fine, test works fine, User, CampaignMember and all the other objects are created fine and callout is made.

I know there can be a problem inserting User and other objects in the same transaction causing Mixed DML exceptions, which I suspected could have something to do with this problem, but the callout is done in a separate transaction than User insert. It is made in a queuable class, which is executed after Test.stopTest(), and it does not collide with insert of any other objects in this or other tests.

Why does this happen with User insert and happens only in the Production execution of the test?

My test code:

    Test.setMock(HttpCalloutMock.class, new SubscribeCampaignMemberMock());

    Campaign campaign = UnitTestFactory.createCampaign('Test Campaign');
    insert campaign;

    Account account = UnitTestFactory.createAccount('Test Account');
    insert account;

    Contact contact1 = UnitTestFactory.createContact('Test Contact 1',
          'testcontact1@testcontact.test', account.Id);
    Contact contact2 = UnitTestFactory.createContact('Test Contact 2', 
          'testcontact2@testcontact.test', account.Id);

    insert contact1;
    insert contact2;

    User user = UnitTestFactory.createUser('testuser@testuser.test');

    // If I remove this insert, test will work 
    insert user;

        CampaignMember campaignMember1 = 
             UnitTestFactory.createCampaignMember(campaign, contact1);
        CampaignMember campaignMember2 = 
             UnitTestFactory.createCampaignMember(campaign, contact2);

    // This insert triggers creation of queueable
    insert new CampaignMember[]{campaignMember1, campaignMember2};

    Test.startTest();
    System.enqueueJob(new ActivateCampaignMember(new Campaign[] {campaign}));

    // This call to Test.stopTest() executes queueable, callout is 
    // executed and exception is thrown
    Test.stopTest();

    // Assert
    ...

Please don't mind the UnitTestFactory methods, they just return new instances of objects with mandatory fields populated.

EDIT 1:

I also tried to encapsulate the user creation to a System.runAs() block, but it did not solve the issue. Sandboxes I tried deployment on were Developer and Partial Copy freshly refreshed from Production.

EDIT 2:

I tried wrapping

    Test.startTest();
    System.enqueueJob(new ActivateCampaignMember(new Campaign[] {campaign}));
    Test.stopTest();

in a System.runAs() block and the exception is still thrown.

EDIT 3:

I tried moving the User creation logic to @TestSetup. All 13 tests in the test class started getting CalloutException on deployment to Production, but again, they all passed on deployment to Partial Copy sandbox.

EDIT 4:

I compared the logs from test run in Partial Copy sandbox and Production, nothing except timestamps and Ids of jobs and objects is different on log level granularity levels of Database - Finest, Callouts - Info, Apex Code - Debug. The difference is only in the end of log, where Prod throws exception:

Partial copy sandbox log:

CODE_UNIT_STARTED|[EXTERNAL]|01p58000009Emts|ActivateCampaignMemberTest.membersWithExistingEditorsAreSkipped
CODE_UNIT_STARTED|[EXTERNAL]|01p5E000000KSFJ|SubscribeCampaignMember
USER_DEBUG|[34]|DEBUG|Executing Campaign Member subscription callout with queueable job id 7075E00000NYiHsQAL
CODE_UNIT_FINISHED|SubscribeCampaignMember
CODE_UNIT_STARTED|[EXTERNAL]|01p58000009Emtr|ActivateCampaignMember
CODE_UNIT_FINISHED|ActivateCampaignMember    
CODE_UNIT_FINISHED|ActivateCampaignMemberTest.membersWithExistingEditorsAreSkipped

Production log:

CODE_UNIT_STARTED|[EXTERNAL]|01p58000009Emts|ActivateCampaignMemberTest.membersWithExistingEditorsAreSkipped
CODE_UNIT_STARTED|[EXTERNAL]|01p58000009Ev2l|SubscribeCampaignMember
USER_DEBUG|[34]|INFO|Executing Campaign Member subscription callout with queueable job id 7075800004EUdslAAD
EXCEPTION_THROWN|[19]|System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out
FATAL_ERROR|System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out
Class.HttpService.put: line 19, column 1
Class.SubscribeCampaignMember.execute: line 47, column 1
CODE_UNIT_FINISHED|SubscribeCampaignMember
FATAL_ERROR|System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out
Class.HttpService.put: line 19, column 1
Class.SubscribeCampaignMember.execute: line 47, column 1
FATAL_ERROR|System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out
Class.HttpService.put: line 19, column 1
Class.SubscribeCampaignMember.execute: line 47, column 1
CODE_UNIT_FINISHED|ActivateCampaignMemberTest.membersWithExistingEditorsAreSkipped
  • I have moved the extended debugging conversation to chat. – Adrian Larson Feb 23 '18 at 16:30
  • 2
    What's the use of moving these discussions to chat when it gets deleted before anyone goes back to update the thread with whatever solved it??? – Charles T Apr 25 '18 at 11:55

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.