7.5 KiB
Performance Optimization for Apex Tests
Fast tests enable faster development. When test suites run quickly, developers refactor confidently. This guide covers techniques to dramatically reduce test execution time.
Why Test Speed Matters
| Test Suite Duration | Impact |
|---|---|
| < 5 minutes | Developers run frequently, catch issues early |
| 5-30 minutes | Developers run occasionally, issues slip through |
| 30+ minutes | Developers avoid running, tests become stale |
| Hours | CI/CD bottleneck, blocked deployments |
The goal: Sub-second unit tests, with integration tests taking seconds, not minutes.
Technique 1: Mock DML Operations
Database operations are the #1 cause of slow tests.
The Numbers
| Operation | 10,000 Records | Notes |
|---|---|---|
| Actual insert | ~50 seconds | Database round-trips |
| DML mocking | <1 second | In-memory only |
| Improvement | ~35x faster |
Implementation
See assets/dml-mock.cls and references/mocking-patterns.md for complete implementation.
// ❌ SLOW: Actual database insert
List<Account> accounts = TestDataFactory.createAccounts(1000);
insert accounts; // ~5 seconds
// ✅ FAST: Mock DML
DMLMock.reset();
AccountService service = new AccountService(new DMLMock());
service.createAccounts(accounts); // <0.1 seconds
Assert.areEqual(1000, DMLMock.InsertedRecords.size());
Technique 2: Mock SOQL Queries
Query execution adds overhead, especially with large result sets.
// ❌ SLOW: Actual query requiring test data setup
@TestSetup
static void setup() {
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 1000; i++) {
accounts.add(new Account(Name = 'Test ' + i));
}
insert accounts; // Slow
}
// ✅ FAST: Mock query results
AccountSelector.setMockResults(new List<Account>{
new Account(Name = 'Mock 1'),
new Account(Name = 'Mock 2')
});
List<Account> results = AccountSelector.getActiveAccounts(); // Instant
Technique 3: Minimize @TestSetup
@TestSetup runs before every test method. Large setups compound execution time.
// ❌ SLOW: Heavy @TestSetup
@TestSetup
static void setup() {
List<Account> accounts = TestDataFactory.createAccounts(100);
insert accounts;
List<Contact> contacts = TestDataFactory.createContacts(500, accounts);
insert contacts;
List<Opportunity> opps = TestDataFactory.createOpportunities(200, accounts);
insert opps;
// Total: 800 DML operations, runs before EACH test method
}
// ✅ FAST: Minimal @TestSetup, mock what you can
@TestSetup
static void setup() {
// Only create what MUST exist in database
Account parentAccount = new Account(Name = 'Required Parent');
insert parentAccount;
}
Technique 4: Choose Efficient Loop Constructs
Loop performance varies significantly with large iterations.
Benchmark Results (10,000 iterations)
| Loop Type | Duration | Notes |
|---|---|---|
| While loop | ~0.4s | Fastest |
| Cached iterator | ~0.8s | Good alternative |
| For loop (index) | ~1.4s | Acceptable |
| Enhanced for loop | ~2.4s | Convenient but slower |
| Uncached iterator | CPU limit | Avoid |
Recommendation
// ✅ PREFERRED: While loop for large iterations
Iterator<Account> iter = accounts.iterator();
while (iter.hasNext()) {
Account acc = iter.next();
// process
}
// ✅ ACCEPTABLE: Standard for loop
for (Integer i = 0; i < accounts.size(); i++) {
Account acc = accounts[i];
// process
}
// ⚠️ CONVENIENT BUT SLOWER: Enhanced for
for (Account acc : accounts) {
// process
}
Technique 5: Batch Test Data Creation
Creating records one-by-one is slow. Batch operations are faster.
// ❌ SLOW: One-by-one creation
for (Integer i = 0; i < 200; i++) {
Account acc = new Account(Name = 'Test ' + i);
insert acc; // 200 DML statements!
}
// ✅ FAST: Batch creation
List<Account> accounts = new List<Account>();
for (Integer i = 0; i < 200; i++) {
accounts.add(new Account(Name = 'Test ' + i));
}
insert accounts; // 1 DML statement
Technique 6: Use Assert Instead of System.assert
The modern Assert class is cleaner and provides better error messages.
// ❌ OLD: System.assert (still works but verbose)
System.assert(result != null, 'Result should not be null');
System.assertEquals(expected, actual, 'Values should match');
// ✅ MODERN: Assert class (Apex 56.0+)
Assert.isNotNull(result, 'Result should not be null');
Assert.areEqual(expected, actual, 'Values should match');
Assert.isTrue(condition, 'Condition should be true');
Assert.fail('Should not reach here');
Technique 7: Avoid SOSL in Tests
SOSL searches return empty results in tests unless configured.
// ❌ PROBLEM: SOSL returns nothing in tests by default
List<List<SObject>> results = [FIND 'test' IN ALL FIELDS RETURNING Account];
// results[0] is EMPTY even with matching records!
// ✅ SOLUTION: Use Test.setFixedSearchResults()
@IsTest
static void testSearch() {
Account acc = new Account(Name = 'Searchable');
insert acc;
// Configure what SOSL will return
Test.setFixedSearchResults(new List<Id>{ acc.Id });
Test.startTest();
List<List<SObject>> results = [FIND 'test' IN ALL FIELDS RETURNING Account];
Test.stopTest();
Assert.areEqual(1, results[0].size(), 'Should find configured record');
}
Technique 8: Strategic Test Method Scoping
Run only the tests you need during development.
# ❌ SLOW: Run all tests (minutes to hours)
sf apex run test --test-level RunLocalTests --target-org sandbox
# ✅ FAST: Run single test class
sf apex run test --class-names MyClassTest --target-org sandbox
# ✅ FASTER: Run single test method
sf apex run test --tests MyClassTest.testSpecificMethod --target-org sandbox
Technique 9: Async Test Execution
Use async mode for large test suites to avoid blocking.
# Start tests asynchronously
sf apex run test --class-names MyClassTest --wait 0 --target-org sandbox
# Returns test run ID: 707xx0000000000
# Check status later
sf apex get test --test-run-id 707xx0000000000 --target-org sandbox
Anti-Patterns to Avoid
| Anti-Pattern | Problem | Solution |
|---|---|---|
| DML in loops | N operations instead of 1 | Bulk DML outside loops |
| Large @TestSetup | Runs before every test | Minimize or mock |
| No mocking | Full database round-trips | Mock DML, queries, callouts |
| SeeAllData=true | Depends on org data | Create test data |
| Deep nested loops | O(n²) or worse | Flatten with Maps |
| String concatenation in loops | New string objects each iteration | Use List and join |
Optimization Checklist
Before committing tests, verify:
- DML operations are mocked where possible
- @TestSetup is minimal
- No SOQL/DML inside loops
- Uses bulk patterns (200+ records)
- Individual test methods run in <1 second
- Full test class runs in <10 seconds
- Uses Assert class (not System.assert)
Performance Testing Your Tests
@IsTest
static void testPerformance() {
Long startTime = System.currentTimeMillis();
// Your test code here
Long duration = System.currentTimeMillis() - startTime;
System.debug('Test duration: ' + duration + 'ms');
// Assert performance constraint
Assert.isTrue(duration < 1000, 'Test should complete in <1 second, took: ' + duration + 'ms');
}