Skip to main content

SF-0361 · Coding · Medium

How to write the test class for a future method?

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

The trick: wrap the future-triggering code in Test.startTest() / Test.stopTest(). Calls made between those two execute asynchronously in the live org, but in a test they complete before stopTest() returns — synchronously, on demand — so your assertions run after the future has actually executed.

The class under test

public class AccountStatusUpdater {
    @future
    public static void markActive(List<Id> accountIds) {
        List<Account> accs = [SELECT Id, Status__c FROM Account WHERE Id IN :accountIds];
        for (Account a : accs) a.Status__c = 'Active';
        update accs;
    }
}

The test

@isTest
private class AccountStatusUpdaterTest {

    @isTest
    static void markActive_setsStatusOnAllAccounts() {
        // Arrange: create test data
        List<Account> accs = new List<Account>();
        for (Integer i = 0; i < 5; i++) {
            accs.add(new Account(Name = 'Test ' + i, Status__c = 'Prospect'));
        }
        insert accs;
        List<Id> ids = new List<Id>();
        for (Account a : accs) ids.add(a.Id);

        // Act: invoke the future inside startTest/stopTest
        Test.startTest();
        AccountStatusUpdater.markActive(ids);
        Test.stopTest(); // future runs here, before this returns

        // Assert
        List<Account> after = [SELECT Status__c FROM Account WHERE Id IN :ids];
        for (Account a : after) {
            System.assertEquals('Active', a.Status__c);
        }
    }
}

That’s the whole pattern. The key line is Test.stopTest() — it blocks until all async jobs queued between startTest and stopTest finish.

Callout in a future method

If the future makes a callout, you also need a mock:

@isTest
static void markActive_callsExternalApi() {
    Test.setMock(HttpCalloutMock.class, new MyMockResponse());

    List<Id> ids = createTestAccounts();

    Test.startTest();
    AccountStatusUpdater.syncToErp(ids);
    Test.stopTest();

    // Mock recorded the call — assert request body, status, etc.
}

private class MyMockResponse implements HttpCalloutMock {
    public HttpResponse respond(HttpRequest req) {
        HttpResponse res = new HttpResponse();
        res.setStatusCode(200);
        res.setBody('{"ok":true}');
        return res;
    }
}

Test.setMock intercepts any HTTP callout during the test transaction — including those inside the future.

What Test.startTest()/Test.stopTest() actually do

MethodEffect
Test.startTest()Resets governor limits, opens a “test context”
Async work between themBuffered, not actually executed yet
Test.stopTest()Runs all buffered async work synchronously, then closes the test context

Without startTest/stopTest, the future method does not run in the test — and AsyncApexJob stays empty.

Things that catch people out

  • Only one Test.stopTest() per test method. Calling future-then-stopTest-then-future-then-stopTest doesn’t work as you’d hope.
  • DML must happen before startTest if you want it to count against the parent test’s limits, not the future’s.
  • Callouts inside the future still need Test.setMock — there’s no “auto-pass.”
  • Chained jobs — if your future enqueues a Queueable, stopTest only runs the first level. You may need to assert on the second level separately or refactor.

Verifying the future actually ran

You can also assert via AsyncApexJob:

Test.stopTest();
List<AsyncApexJob> jobs = [
    SELECT Status, MethodName FROM AsyncApexJob
    WHERE JobType = 'Future' AND ApexClass.Name = 'AccountStatusUpdater'
];
System.assertEquals(1, jobs.size());
System.assertEquals('Completed', jobs[0].Status);

Verified against: Apex Developer Guide — Testing Future Methods. Last reviewed 2026-05-17.