In my example, as a part of the PostInstall (class implements InstallHandler) script I will be performing two actions;
- sending an email to the installing User for the express purpose of pointing that Admin user at a Post Install wizard to handhold through a few essential steps (lightning component).
- Setting up a scheduled job who purpose it is to callout to an external endpoint, and start an asynchronous job.

When trying to run the test class the issue "System.CalloutException: You have uncommitted work pending. Please commit or rollback before calling out" occurs.
The test class is simply testing those two actions.
- Did an email get sent to the correct User?
- Did the daily job get kicked off correctly?

Suspected Cause:
The email and the scheduled job are all running in the same context because of the Test.startTest() and Test.stopTest() that is wrapping the post install code.

However in a real life situation, the PostInstall handler class will run, send the email and schedule the sched Apex class. Then in a different context, the scheduled Apex will run ( which performs the API Callouts) and we are avoiding the DML before API Callout issue.

I understand we should not be committing DML before an API Callout occurs however in the real world example this does not occur. Only in the test class where they're essentially being forced into the same context (my best guess).

PostInstall Class

global void onInstall(InstallContext context) {
   //Get the time to kick off this job in the local Users timezone at 2AM
   Date schedDate = Date.today().addDays(1);
   Time schedTime = Time.newInstance(2,0,0,0);

   Datetime schedDateTime = Datetime.newInstance(schedDate, schedTime);

        new Utils.CronUtil().generateCronExpression(schedDateTime),
        new ScheduledApexClass()

    //Send link to post install experience

PostInstallTest Class

static void testInstallScript() {
        SingleMockRequest fakeCreateExportJobResp = new SingleMockRequest(
        Map<String, HttpCalloutMock> endpoint2TestResp = new Map<String,HttpCalloutMock>();

        endpoint2TestResp.put(FakeResponseUtil.TEST_ACCESS_TOKEN_URL, FakeResponseUtil.TEST_FAKE_GENERATE_KEY);
        endpoint2TestResp.put(FakeResponseUtil.TEST_CREATE_JOB_URL, fakeCreateExportJobResp);

        Datetime launchTime = Datetime.now().addMinutes(1000);
        String timeCRON = new Utils.CronUtil().generateCronExpression(launchTime);

        HttpCalloutMock multiCalloutMock = new MultipleMockRequest(endpoint2TestResp);
        Test.setMock(HttpCalloutMock.class, multiCalloutMock);

        Integer emailsSent = Limits.getEmailInvocations();
        PostInstall postInstall = new PostInstall();

        Test.testInstall(postInstall, null);

        List<CronJobDetail> schedJob = [
            SELECT  JobType,
            FROM    CronJobDetail
            WHERE   Name = 'PostInstall'
            AND     JobType = '7'
        System.assert(!schedJob.isEmpty(), 'A Scheduled Apex job with the title of the PostInstall has been created as a part of the Post Install process');

        System.assert(Limits.getEmailInvocations()>emailsSent, 'Email should be sent on Installation');

I guess my ask is;
Do I need to refactor the code for the purposes of this test class
Add something to the tune of a Test.isRunningTest() in the PostInstall handler call to the email handler and then test the email method logic separately?

  • 1
    I'm curious whether the behavior changes if you remove Test.startTest() and Test.stopTest(), since it looks like you don't strictly need them here. I'm not sure the behavior makes sense either way, though! – David Reed Jul 14 '19 at 23:33
  • That seems to have fixed it. So used to using Test.startTest() + Test.stopTest() that didn't even occur to me. Feel free to post as answer and I'll mark it as the answer. – JetSparx Jul 15 '19 at 23:05

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.