1

This has been asked here and here, but I'm not satisfied with the answers. Hoping to gain a better understanding of this.

In working through the Apex Specialist Superbadge, the callout portion of the challenge consists of a Schedulable that calls a future. While unit testing this, it became apparent that you can't assert behavior of the future method after scheduling the Schedulable class in a unit test, presumably because Salesforce is waiting only for asynchronous code executed directly from the unit test to complete, and doesn't wait for asynchronous code called from asynchronous code to complete.

Below is a simplified example of what I'm talking about.

public with sharing class ExampleFuture {
    public static Boolean doSomethingInvoked = false;
    
    @Future(Callout=true)
    public static void doSomething() {
        doSomethingInvoked = true;
    }
}

public with sharing class ExampleSchedulable implements Schedulable {
    public void execute(SchedulableContext context) {
        ExampleFuture.doSomething();
    }
}

@IsTest
private class ExampleSchedulableTest {
    @IsTest
    private static void testBehavior() {
        Test.startTest();
        System.schedule('ExampleSchedulable', '0 0 0 15 3 ? 2022', new ExampleSchedulable());
        Test.stopTest();

        // This fails
        System.assert(ExampleFuture.doSomethingInvoked);
    }
}

The assertion in the test class fails, even though Salesforce documentation states

All asynchronous calls made after the startTest method are collected by the system. When stopTest is executed, all asynchronous processes are run synchronously.

Again, I presume they're referring to top-level asynchronous code executed directly from the unit test only, and not anything deeper in the call stack, but it's not obvious.

The weird thing is, executing the unit test with code coverage, and without the assertion, yields 100% coverage in the future method. So it seems the future method is getting called synchronously.

Of course in a real world example I would mock the future call with a spy, but I would love a real explanation of what is happening here.

Any insight is appreciated!

2
  • 2
    IMHO you should consider such chained async invocations to be quite separate and should simply test that the schedulable causes an async job to be submitted. What you are doing is not unit testing but rather trying to "integration test" an entire async flow. I would recommend adding a separate unit test that directly tests the future method. This is then more like unit testing where small parts of your code are tested in isolation.
    – Phil W
    Commented Jul 5, 2020 at 5:37
  • @PhilW thanks for the comment! I agree, hence my comment on a spy, but I'm just looking to gain a better understanding with this.
    – Matt
    Commented Jul 5, 2020 at 11:42

1 Answer 1

2

While unit testing this, it became apparent that you can't assert behavior of the future method after scheduling the Schedulable class in a unit test, presumably because Salesforce is waiting only for asynchronous code executed directly from the unit test to complete, and doesn't wait for asynchronous code called from asynchronous code to complete

This is an accurate summation. We also discuss is in our canonical resource, How do I unit test Asynchronous Apex?.

The general recommendation for dealing with this situation is to decompose the test. You cannot integration-test a sequence of async functionality, but you can unit-test each element in isolation. And further, you can use strategies like dependency injection (in some situations only!) to test the relationships between your async layers. Note that you won't be able to DI async code that is run async because its parameters are serialized and persisted, meaning that any object you inject as a dependency won't be the same instance that's used when the async execution takes place. DI can be used by running Queueable and Batch Apex code synchronously (explicitly calling start(), execute(), et al.) to validate behavior.

The weird thing is, executing the unit test with code coverage, and without the assertion, yields 100% coverage in the future method. So it seems the future method is getting called synchronously.

The transaction structure and overall behavior of a unit test that uses Test.stopTest() and runs Asynchronous Apex is quite unintuitive. In addition to code coverage anomalies, you may also see in your debug logs that the future method does execute within log scope - but you still cannot write assertions against its results. That's just a quirk of the platform and how it "flattens" multiple transactions' worth of code into one.

1
  • Exactly the answer I was looking for. Thank you!
    – Matt
    Commented Jul 5, 2020 at 15:35

You must log in to answer this question.

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