Skip to main content

SF-9208 · Coding · Medium

How do you test asynchronous Apex?

✓ Verified by Vikas Singhal · Last reviewed 5/17/2026 · Updated for Spring '26

Asynchronous Apex is designed to run later, after the current transaction. That’s a problem for tests, which need a deterministic result now. Salesforce solves this with a special rule: anything queued between Test.startTest() and Test.stopTest() runs synchronously when stopTest() is called.

The core pattern

@isTest
static void testQueueable() {
    List<Account> accounts = new List<Account>{
        new Account(Name = 'A'), new Account(Name = 'B')
    };
    insert accounts;

    Test.startTest();
    System.enqueueJob(new SyncToErpQueueable(accounts));
    Test.stopTest(); // <-- Queueable runs to completion HERE

    // Now assert
    List<Account> updated = [SELECT Id, ERP_Synced__c FROM Account];
    for (Account a : updated) {
        System.assertEquals(true, a.ERP_Synced__c);
    }
}

The Queueable would normally execute seconds or minutes later. Inside the startTest/stopTest block, it runs inline.

Per async type

@future methods

Test.startTest();
MyService.doAsync(accountIds); // calls @future method
Test.stopTest();
// @future has now executed

Queueable

Test.startTest();
System.enqueueJob(new MyQueueable(data));
Test.stopTest();
// Queueable.execute() has run

Batch Apex

Test.startTest();
Database.executeBatch(new MyBatch(), 200);
Test.stopTest();
// Batch start, execute (with one chunk only), and finish all ran

Important: in tests, Batch executes only one chunk. If your data exceeds the chunk size (200 by default), only the first 200 records are processed. To test bulk behavior, keep test data within the chunk size.

Scheduled Apex

Test.startTest();
String cron = '0 0 0 1 1 ? 2099';
System.schedule('Test Job', cron, new MyScheduler());
Test.stopTest();
// Scheduler.execute() has run, regardless of the cron expression

The cron expression is ignored in tests — Test.stopTest() forces immediate execution.

Async chaining

If your Queueable enqueues another Queueable, only the first level executes inside the test. The chained job is queued but doesn’t run.

// In production code:
public void execute(QueueableContext ctx) {
    // do work
    if (moreWork) System.enqueueJob(new MyQueueable(remaining)); // <-- this won't run in tests
}

Workaround: assert against the queued job state, then in a separate test, exercise the chained path directly.

@isTest
static void chainedJob_isEnqueued() {
    Test.startTest();
    System.enqueueJob(new MyQueueable(initial));
    Test.stopTest();

    List<AsyncApexJob> jobs = [SELECT Id, Status FROM AsyncApexJob WHERE JobType = 'Queueable'];
    System.assertEquals(2, jobs.size(), 'Original + chained job queued');
}

System.runAs — running tests as another user

Distinct from async testing, but commonly confused. System.runAs lets you run a block of code as a specified user, useful for testing sharing rules, profile permissions, and record visibility:

@isTest
static void salesUser_cannotSeeOpsRecords() {
    User salesUser = [SELECT Id FROM User WHERE Profile.Name = 'Sales' LIMIT 1];

    System.runAs(salesUser) {
        List<Account> visible = [SELECT Id FROM Account WHERE Department__c = 'Ops'];
        System.assertEquals(0, visible.size());
    }
}

System.runAs works in any test context. It’s not async-specific — it’s the right way to test FLS, sharing, and profile-based access.

Things that catch people

  • Forgetting Test.stopTest() — async jobs never execute, assertions fail mysteriously.
  • Mixing async types — you can only call startTest/stopTest once per method. Test each async type separately.
  • Callouts inside async — still need Test.setMock before the async is queued.
  • Batch’s chunk size in tests — only the first chunk runs. Don’t put 500 records in if you’re testing 200-record chunking.

Verified against: Apex Developer Guide — Testing Asynchronous Apex. Last reviewed 2026-05-17 for Spring ‘26 release.