Test a batch by submitting it between Test.startTest() and Test.stopTest(). The async batch runs synchronously inside stopTest, so by the time stopTest returns, all chunks have completed and you can assert on the final state.
The basic pattern
@isTest
private class StaleCaseCloserBatchTest {
@testSetup
static void setup() {
List<Case> cases = new List<Case>();
for (Integer i = 0; i < 5; i++) {
cases.add(new Case(
Subject = 'Test ' + i,
Status = 'New',
Description = 'Original'
));
}
insert cases;
// Backdate to make them "stale"
Test.setCreatedDate(cases[0].Id, Datetime.now().addDays(-60));
Test.setCreatedDate(cases[1].Id, Datetime.now().addDays(-45));
}
@isTest
static void cleanup_closesStaleCases() {
Test.startTest();
Database.executeBatch(new StaleCaseCloserBatch(), 200);
Test.stopTest(); // batch runs to completion here
List<Case> closed = [SELECT Id, Status FROM Case WHERE Status = 'Auto-Closed'];
System.assertEquals(2, closed.size(), 'Both backdated cases should be closed');
}
}
Three things to know:
Test.startTest()andTest.stopTest()form an async boundary. Async work submitted between them runs synchronously insidestopTest.- Only one chunk runs in test. The batch’s
startreturns up to 200 records (or 50,000 for QueryLocator), andexecuteis called once with all of them. The chunking you’d see in production doesn’t happen in test. - Both
executeandfinishare called. Full lifecycle.
What “only one chunk” means
This is the big test-vs-production difference. In test mode, the batch runs in a single transaction with all the records. So:
- Per-chunk governor limits apply to the full dataset, not per real chunk.
Database.Statefulis not exercised the same way — there’s only oneexecute, so cross-chunk persistence isn’t really tested.- Tests don’t catch chunk-order bugs.
If you absolutely need to test stateful behavior across chunks, you have to mock the chunks yourself by calling execute multiple times.
Testing with multiple chunks (manual)
@isTest
static void execute_accumulatesStateAcrossChunks() {
StaleCaseCloserBatch b = new StaleCaseCloserBatch();
Database.BatchableContext ctx = null; // tests run sync, ctx not needed
// First "chunk"
b.execute(ctx, [SELECT Id FROM Case WHERE Status = 'New' LIMIT 2]);
// Second "chunk"
b.execute(ctx, [SELECT Id FROM Case WHERE Status = 'New' LIMIT 2 OFFSET 2]);
System.assertEquals(4, b.totalProcessed);
}
Not a true simulation (real chunks would be fresh instances without Stateful), but enough to assert that your execute accumulates correctly.
Testing callouts
If the batch implements Database.AllowsCallouts, mock the HTTP:
@isTest
static void syncBatch_callsExternalApi() {
Test.setMock(HttpCalloutMock.class, new MyMockResponse());
Test.startTest();
Database.executeBatch(new SyncBatch(), 5);
Test.stopTest();
// Assert records updated based on mocked response
}
Asserting on AsyncApexJob
You can also assert that the job ran cleanly:
Test.stopTest();
AsyncApexJob job = [
SELECT Status, NumberOfErrors FROM AsyncApexJob
WHERE JobType = 'BatchApex' AND ApexClass.Name = 'StaleCaseCloserBatch'
];
System.assertEquals('Completed', job.Status);
System.assertEquals(0, job.NumberOfErrors);
Common test coverage targets
| Scenario | Test method |
|---|---|
| Happy path | Normal data, assert final state |
| Empty start | start returns no records → finish still runs |
| Errored chunks | Cause an exception in execute → assert NumberOfErrors > 0 |
| Stateful aggregation | Call execute twice manually, assert cumulative state |
| Callouts | Mock with Test.setMock |
finish side effects | Assert email sent, next batch chained, etc. |
Common interview follow-ups
- Why does only one chunk run in test? — Test framework constraint. Async work runs synchronously and once inside
Test.stopTest(). - Does
@testSetupdata persist for the batch? — Yes — the batch’sstartquery sees@testSetupdata normally. - Can I exceed 50K records in test? — No. Even though QueryLocator allows 50M in production, test data is constrained by what you can
insertin setup (50K total DML rows).
Verified against: Apex Developer Guide — Testing Batch Apex. Last reviewed 2026-05-17.