4

According to Salesforce docs:

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

Now I was assuming this holds true for all async apex, but for the queueable apex, it executes even if we don't enqueue a job between start test and stop test.

Minimum Viable Code to reproduce:

Queueable Apex :

public class MyQueueableApex implements Queueable{
    public void execute(QueueableContext context) {
        System.debug('Inside Queueable apex');
    }
}

Test class:

@isTest
public class MyQueueableApexTest {

    @isTest
    public static void queuableTest(){

        Id jobId = System.enqueueJob(new MyQueueableApex());
        System.assertNotEquals('Completed' , [SELECT Id , Status FROM AsyncApexJob WHERE Id=:jobId][0].Status);
    }
}

Debug:

48.0 APEX_CODE,DEBUG;APEX_PROFILING,INFO;CALLOUT,INFO;DB,INFO;NBA,INFO;SYSTEM,DEBUG;VALIDATION,INFO;VISUALFORCE,INFO;WAVE,INFO;WORKFLOW,INFO
16:24:33.0 (404217)|USER_INFO|[EXTERNAL]|00528000000PIGY|pjxdsfse@gmail.com|(GMT+00:00) Greenwich Mean Time (Europe/London)|GMTZ
16:24:33.0 (445053)|EXECUTION_STARTED
16:24:33.0 (450671)|CODE_UNIT_STARTED|[EXTERNAL]|01p0I00000GAtQS|MyQueueableApexTest.queuableTest()
16:24:33.0 (11522220)|SOQL_EXECUTE_BEGIN|[8]|Aggregations:0|SELECT Id, Status FROM AsyncApexJob WHERE Id = :tmpVar1
16:24:33.0 (15845130)|SOQL_EXECUTE_END|[8]|Rows:1
16:24:33.16 (16115510)|CUMULATIVE_LIMIT_USAGE
16:24:33.16 (16115510)|LIMIT_USAGE_FOR_NS|(default)|
  Number of SOQL queries: 1 out of 100
  Number of query rows: 1 out of 50000
  Number of SOSL queries: 0 out of 20
  Number of DML statements: 0 out of 150
  Number of DML rows: 0 out of 10000
  Maximum CPU time: 0 out of 10000
  Maximum heap size: 0 out of 6000000
  Number of callouts: 0 out of 100
  Number of Email Invocations: 0 out of 10
  Number of future calls: 0 out of 50
  Number of queueable jobs added to the queue: 1 out of 50
  Number of Mobile Apex push calls: 0 out of 10

16:24:33.16 (16115510)|CUMULATIVE_LIMIT_USAGE_END

16:24:33.0 (16157837)|CODE_UNIT_FINISHED|MyQueueableApexTest.queuableTest()
16:24:33.0 (17551568)|EXECUTION_FINISHED
16:24:33.27 (27032903)|USER_INFO|[EXTERNAL]|00528000000PIGY|pjxdsfse@gmail.com|(GMT+00:00) Greenwich Mean Time (Europe/London)|GMTZ
16:24:33.27 (27047994)|EXECUTION_STARTED
16:24:33.27 (27051597)|CODE_UNIT_STARTED|[EXTERNAL]|01p0I00000GAtQN|MyQueueableApex
16:24:33.27 (28544098)|USER_DEBUG|[3]|DEBUG|Inside Queueable apex
16:24:33.27 (28634595)|CODE_UNIT_FINISHED|MyQueueableApex
16:24:33.27 (29986879)|EXECUTION_FINISHED

The status of async-apex job is still Pending, I can see from debug log. execute method was called even if I didn't use start , stop test at all. Is it expected behaviour documented somewhere?

The issue I faced is, I was unit-testing if queueable is being queued with dummy value, I didn't care about if it was successful or not, but my unit test broke cause Apex Test execute queueabale, and due to dummy values, SOQL failed.

Also this document states:

The ID of a queueable Apex job isn’t returned in test context—System.enqueueJob returns null in a running test.

I could still see enqueueJob returning proper job id and not null.

3

I wrote a unit test that describes how all asynchronous code runs at the end of a unit test. This was in 2018, and still not fixed (and likely won't be).

The documentation is also incorrect, as far as I can tell (System.enqueueJob returning an ID in test context). Report a bug on Twitter to @salesforcedocs. They'll put something in for you.

If you do not call Test.stopTest, but you still want to kill any potential asynchronous jobs, use Database.rollback.

Savepoint sp = Database.setSavePoint();
Test.startTest();
// Do stuff here //
Database.rollback(sp);

Doing this will roll back all parts of the transaction, including calling future methods, queueable methods, scheduled methods, batchable methods, emails, post-commit platform events, etc.

| improve this answer | |
4

Well this is a great find. Lots of undocumented / poorly documented behavior as far as I can tell :p.

First, I verified on my end that I also am now receiving a job id in tests. I modified your unit test to verify that this happens both inside and outside of a Test.start/stop context. In both cases, we are now receiving job ids.

@isTest
public class MyQueueableApexTest {

    @isTest
    public static void queuableTestWithoutStartStop(){

        Id jobId = System.enqueueJob(new MyQueueableApex());
        System.debug('queuableTestWithoutStartStop [jobId]: ' + jobId);
        System.assertNotEquals('Completed' , [SELECT Id , Status FROM AsyncApexJob WHERE Id=:jobId][0].Status);

    }

    @isTest
    public static void queuableTestWithStartStop(){

        Test.startTest();
        Id jobId = System.enqueueJob(new MyQueueableApex());
        System.debug('queuableTestWithStartStop [jobId]: ' + jobId);
        Test.stopTest();

        System.assertEquals('Completed' , [SELECT Id , Status FROM AsyncApexJob WHERE Id=:jobId][0].Status);

    }
}

I couldn't find any release notes documenting this change. I did find that 85 people reported this as a problem on the trailblazer community, and Salesforce classified it as a "Known Issue - No Fix". So since it was considered an issue/bug, perhaps it got fixed?

Concerning the main question of your post, I believe this is intended behavior, though the phrasing of the documentation could certainly use some clarity. It sounds like the purpose of startTest and stopTest are not to control if asynchronous jobs are run during a test, but rather to control when, and under what context. From the Test class documentation:

You can also use this method with stopTest to ensure that all asynchronous calls that come after the startTest method are run before doing any assertions or testing.

The earlier document you linked has a paragraph which I think might explain why we see this behavior:

The startTest method does not refresh the context of the test: it adds a context to your test. For example, if your class makes 98 SOQL queries before it calls startTest, and the first significant statement after startTest is a DML statement, the program can now make an additional 100 queries. Once stopTest is called, however, the program goes back into the original context, and can only make 2 additional SOQL queries before reaching the limit of 100.

This is speaking of how DML limits behave inside and outside the start/stop methods, but I think it gives some insight into how async works as well. It sounds like Salesforce has something like a "context stack" that it uses to provide different limits to different pieces of code. Some managed packages for example get their own limits, and we see in the debug logs that as code enters and exits that context, the limits are added to and removed from the stack. Test.startTest/stopTest also use this framework to provide a new context when start and stop are used.

If we say that asynchronous jobs in a test context are run when that context is removed from the stack, then we'd see the following behavior:

  1. Async jobs between start/stop are run when stop is called.
  2. Async jobs between the beginning and end of a test method are run when the test method completes

So the long and the short of it is, async jobs will always execute within the confines of a test, but if you want to run assertions against the results of the test, then you need to wrap it with the start/stop method calls.

In my company's org, we found that a good design pattern for queueables is to always avoid enqueuing them within a test, by protecting the enqueue statement:

if (!Test.isRunningTest()) jobId = System.enqueueJob(myQueueable);

This helps to cleanly separate the (typically heavier) async processing from normal CPU/DML limits being incurred by the code that launches the queueable. Then on the queueable side, structure the logic so that it can be unit tested outside of an asynchronous context.

Hope that helps, -- Nate

| improve this answer | |
  • 1
    Makes sense. From debug log timestamp it looks like queueable ran after assertion and end of test method. Also if the queueable throws exception the original test method also breaks. I will mock queueable for now. Thanks :) – Pranay Jaiswal Mar 15 at 19:30
  • Totally, was a great find on your part :p – Nathanael Schmolze Mar 15 at 19:46
  • an alternate pattern that we use is that you have two unit tests - one to assert that a queueable job was created; the other constructs the queueable object and then calls execute with the constructor args and finally verifies the queueable dos whatever it is supposed to do. This way, you can ignore the OPs raised concern - your approach doesn't allow asserting that a queueable was scheduled – cropredy Mar 15 at 23:52
  • Well the OP's issue was that he couldn't test creating the job without having it actually execute as well, because the unit test logic executes even without Test.stopTest(). So the workaround I suggested is to design the code so that there's no need to test that it was scheduled, by protecting the enqueueJob line with Test.isRunningTest(). Then you unit test the queueable logic by doing what you said -- construct the queueable and test it outside of an async context. I think his suggestion of using mock allows the testing of scheduling though, so that might be the most complete solution. – Nathanael Schmolze Mar 16 at 0:07
  • 1
    You can rollback asynchronous calls, thus allowing you to write a unit test for when you do want to test your complete code, without resorting to Test.isRunningTest hacks in your production code. – sfdcfox Mar 16 at 4:44

Your Answer

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

Not the answer you're looking for? Browse other questions tagged or ask your own question.