21

For the last years I expected all async Apex means not to run in tests when there is no Test.stopTest() in my test code.

But now I have a test without this call and a future method is called.

I thought Test.stopTest() is forcing all async Apex to run instantely.

  1. How does it really work, when do I need Test.stopTest()?
  2. Is this also true for Batch?
  • My assumption is all async operation run in apex but can not be caught without Test.stopTest() – Ashwani May 10 '17 at 11:30
  • What do you mean by "be caught"? – Robert Sösemann May 10 '17 at 11:43
  • 1
    We can't see results in test class. Please see my answer. – Ashwani May 10 '17 at 12:16
17

It sounds like a bug, or at least it is not documented; I would not depend on this behavior. If you want to test asynchronous code, use Test.stopTest(). This applies for Schedulable, Queueable, Batchable, and @future methods. If you're not using Test.stopTest(), then you're obviously not checking the results of the execution, which means you're only writing code coverage, not quality assurance test methods, which is literally the entire point of unit tests. I'll check with someone over at salesforce.com and see if something's changed. In the meantime, you should always use Test.stopTest() and verify the output of any asynchronous code to avoid regression and logic errors when deploying updates to your code. Also, using proper unit testing (by using asserts, for example) will help Salesforce prevent regression bugs when it runs the Apex Hammer Tests each release.


Here's proof that the methods are indeed executing:

@isTest class q173748 implements Database.Batchable<Integer>, Queueable, Schedulable {
    @isTest static void testFuture() {
        failAsync();
    }
    @isTest static void testBatchable() {
        Database.executeBatch(new q173748());
    }
    @isTest static void testQueueable() {
        System.enqueueJob(new q173748());
    }
    @isTest static void testSchedulable() {
        System.schedule('q173748','0 0 0 * * ?', new q173748());
    }
    // ----------------------------------------------------------- //
    public static void fail() {
        System.assert(false);
    }
    @future static void failAsync() {
        fail();
    }
    // ----------------------------------------------------------- //
    public Integer[] start(Database.BatchableContext context) {
        return new Integer[] { 1 };
    }
    public void execute(Database.BatchableContext context, Integer[] scope) {
        fail();
    }
    public void finish(Database.BatchableContext context) {

    }
    // ----------------------------------------------------------- //
    public void execute(QueueableContext context) {
        fail();
    }
    // ----------------------------------------------------------- //
    public void execute(SchedulableContext context) {
        fail();
    }
}

All of these tests fail, because asynchronous code is indeed called at the end of each unit test method.

| improve this answer | |
  • 1
    Thanks Brian, When asking Salesforce please use the wider context of this question which I described here: salesforce.stackexchange.com/questions/173748 – Robert Sösemann May 10 '17 at 12:55
  • 1
    @RobertSösemann I built a test suite that proves they are running. Nothing in the documentation clearly calls this out, as far as I can tell. I'm going to bring this up. Either the documentation team has some explaining to do, or there's a bug. This really should be documented. – sfdcfox May 10 '17 at 13:23
  • Have you any updates on this? I am experiencing the same behavior with batch class tests. – Robert Méndez Jan 25 '18 at 13:11
  • Yes the problem still exists. Robert? Is it you? What are you doing? And where? (Can you please remove the Up2Go from you profile ;-) – Robert Sösemann Jan 26 '18 at 10:34
  • 1
    @RobertSösemann My contact said that I should file a documentation bug so it can be researched. They also stated that it wouldn't be possible to fix this in the near future because it would break tests everywhere (e.g. it would likely cause Hammer Tests to fail). That reminds me, I have a bug to log. – sfdcfox Feb 12 '18 at 16:41
7

If you look at the documentation for Test.stopTest(), it says nothing about when asynchronous code does not run. Rather, it simply states that asynchronous is run synchronously when you call Test.stopTest().

Each test method is allowed to call this method only once. Any code that executes after the stopTest method is assigned the original limits that were in effect before startTest was called. All asynchronous calls made after the startTest method are collected by the system. When stopTest is executed, all asynchronous processes are run synchronously.

Nothing prevents the future from running. Introduce a long enough delay, and the future will have completed.

@IsTest
public class DemoFuture
{
    @future
    public static void createSomeData()
    {
        insert new Account(/*data*/);
    }
    static testmethod void testFutureCall()
    { // fail
        createSomeData();
        system.assertEquals(1, [SELECT count() FROM Account], 'Future not yet run');
    }
    static testmethod void testFutureCall()
    { // pass
        createSomeData();

        // introduce a delay
        List<String> data = new List<String>();
        for (Integer i = 0; i < 1000; i++) data.add('a'.repeat(1000));
        for (Integer i = 0; i < 1000; i++)
            data = (List<String>)JSON.deserialize(JSON.serialize(data), List<String>.class);

        system.assertEquals(1, [SELECT count() FROM Account], 'Future should have run');
    }
}
| improve this answer | |
  • That's not how future methods work. I just tested this, and both methods do fail. – sfdcfox May 10 '17 at 13:22
  • @sfdcfox Works for me in cs14 with API Version 35... – Adrian Larson May 10 '17 at 13:22
  • 1
    If that's true, log a bug. Future methods must not run before a transaction completes, because that could allow future methods to run at inappropriate times (e.g. because a transaction was later rolled back). I can't reproduce it, but this would be a fairly significant problem if it happened. – sfdcfox May 10 '17 at 13:27
7

Test.startTest() makes all asynchronous calls synchronous (instant) in Test methods. It helps us in catch any result obtained by the execution of asynchronous calls.

Below is sample example:-

Test define One future and batch class:-

Future:

public class FutureCallClassMethod 
{
    @future
    public static void futureCall()
    {
        Account acc = new Account(Name='Future Call');
        insert acc;
        System.debug(' Position 1:  Account is created in Future: '+acc);
    }

    public static void BatchCall()
    {
        Database.executeBatch(new BatchClassSample());
        System.debug(' Position 2:  Batch executed');
    }
}

Batch class:-

global class BatchClassSample implements Database.Batchable<sObject>{

   global final String Query;
   global final String Entity;
   global final String Field;
   global final String Value;

   global BatchClassSample(){

      Query='SELECT id FROM Account Limit 10';
   }

   global Database.QueryLocator start(Database.BatchableContext BC){
      return Database.getQueryLocator(query);
   }

   global void execute(Database.BatchableContext BC, List<sObject> scope) {
        Account acc = new Account(Name='Batch Call');
        insert acc;
        System.debug(' ==> Position 1 in Batch class: '+scope);
    }

   global void finish(Database.BatchableContext BC){
   }
}

Test class:-

@isTest
public class FutureCallClassMethodTest 
{
    public static testmethod void runFuture()
    {
        FutureCallClassMethod.futureCall();
        List<Account> acc = [SELECT Id, Name from Account];
        System.debug(' ==> Position 2: Future without Test.startTest: '+acc);
    }

    public static testmethod void runFutureWithTest()
    {
        Test.startTest();
        FutureCallClassMethod.futureCall();
        Test.stopTest();
        List<Account> acc = [SELECT Id, Name from Account];
        System.debug(' ==> Position 2: Future with Test.startTest: '+acc);
    }

    public static testmethod void runBatch()
    {
        Account accTest = new Account(Name='Test Account');
        insert accTest;
        FutureCallClassMethod.batchCall();
        List<Account> acc = [SELECT Id, Name from Account  WHERE Name!='Test Account'];
        System.debug(' ==> Position 3: Batch without Test.startTest: '+acc);
    }

    public static testmethod void runBatchWithTest()
    {
        Account accTest = new Account(Name='Test Account');
        insert accTest;
        Test.startTest();
        FutureCallClassMethod.batchCall();
        Test.stopTest();
        List<Account> acc = [SELECT Id, Name from Account WHERE Name!='Test Account'];
        System.debug(' ==> Position 3: Batch with Test.startTest: '+acc);
    }
}

When you execute the class you can clearly see that:-

  • Without Test.startTest, asynchronous code executes in last and the no account records was found when we queried that records in test class.

  • With Test.startTest asynchronous operation executes in series and result also obtained in test class.

These can be seen in debugs as well. We get zero result of asynchronous operation without Test.StartTest. And these executed in last. enter image description here

Everything executes but their sequence changes.

| improve this answer | |
4

I believe the main reason for Test.startTest() and Test.stopTest() is to get a clean set of limits for your test.

All the limits used to arrange your test case (inserting data, queries etc.) will get a different limit context when you do your test setup before the Test.startTest(). Then, any limit usage after the Test.stopTest() (for instance for asserting) is measured again against the test context.

This allows you to accurately measure the limit usage of your actual code, without contamination from specific limit usage for setting up and asserting your test cases.

https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_testing_tools_start_stop_test.htm

However, in addition to that, you are right that it does influence asynchronous calls (both future as batch) being fired. Using Test.stopTest() will force the future calls to be executed before any assert calls. If you do not use Test.stopTest(), I assume the future calls will execute when the whole test method is finished, including your assertions.

https://developer.salesforce.com/docs/atlas.en-us.apexcode.meta/apexcode/apex_methods_system_test.htm#apex_System_Test_methods

| improve this answer | |

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.