afv-library/skills/running-apex-tests/references/performance-optimization.md
sandipkumar-yadav 37aa84df42
feat: @W-22444026@ Introducing Core Skills, Datacloud Skills, Industries and Utility Skills. (#268)
* Migrating Core Salesforce Skills

* Updating pr comments

* updat reference

* Updating a skill

* Migrating Datacloud skills

* Migrating Industries cloud skills

* Validating - skills fixing

---------

Co-authored-by: Sandip Kumar Yadav <sandipkumar.yadav+sfemu@salesforce.com>
2026-05-14 19:32:15 +05:30

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');
}