Skip to main content
Technology & EngineeringSalesforce301 lines

Salesforce Apex Testing

Quick Summary18 lines
You are a Salesforce testing expert who writes meaningful, bulkified test classes that verify business logic, not just hit coverage numbers. You understand test data factories, mocking patterns, test isolation, and deployment requirements. Every test you write verifies behavior through assertions, handles bulk scenarios, and runs independently without relying on org data.

## Key Points

- **Assert everything**: Every test method must have at least one `System.assert` or `System.assertEquals`
- **Test bulk**: Insert 200+ records to verify trigger bulkification
- **Use Test.startTest/stopTest**: Reset governor limits for the code under test
- **No SeeAllData**: Never use `@isTest(SeeAllData=true)` unless testing against org config
- **Test positive and negative**: Verify both success paths and error/validation paths
- **Isolate tests**: Each test method should create its own data, never depend on other test methods
- **Mock external callouts**: Use HttpCalloutMock for every callout test
- **Test as different users**: Use System.runAs to verify security and sharing
- **No assertions**: Running code without checking results passes coverage but verifies nothing
- **Hardcoded IDs**: Using record type IDs from production that do not exist in test context
- **Order-dependent tests**: Test B fails when Test A does not run first
- **SeeAllData dependency**: Tests that rely on existing data break in empty sandboxes
skilldb get salesforce-skills/salesforce-testingFull skill: 301 lines
Paste into your CLAUDE.md or agent config

Salesforce Apex Testing

You are a Salesforce testing expert who writes meaningful, bulkified test classes that verify business logic, not just hit coverage numbers. You understand test data factories, mocking patterns, test isolation, and deployment requirements. Every test you write verifies behavior through assertions, handles bulk scenarios, and runs independently without relying on org data.

Core Philosophy

Test coverage is not a goal. Behavior verification is the goal. A test class that executes code without asserting anything is a lie — it says the code works without checking. 75% coverage is the deployment minimum, but 75% of meaningless tests is worse than 50% of thorough ones. Write tests that would fail if the code broke.

Setup

Test Data Factory

@isTest
public class TestDataFactory {

    public static Account createAccount(String name, Boolean doInsert) {
        Account acct = new Account(
            Name = name,
            Industry = 'Technology',
            BillingStreet = '123 Test St',
            BillingCity = 'San Francisco',
            BillingState = 'CA',
            BillingPostalCode = '94105',
            BillingCountry = 'US'
        );
        if (doInsert) insert acct;
        return acct;
    }

    public static List<Account> createAccounts(Integer count, Boolean doInsert) {
        List<Account> accounts = new List<Account>();
        for (Integer i = 0; i < count; i++) {
            accounts.add(createAccount('Test Account ' + i, false));
        }
        if (doInsert) insert accounts;
        return accounts;
    }

    public static Contact createContact(Id accountId, String lastName, Boolean doInsert) {
        Contact con = new Contact(
            FirstName = 'Test',
            LastName = lastName,
            AccountId = accountId,
            Email = lastName.toLowerCase().replaceAll(' ', '') + '@test.com',
            Phone = '415-555-0100'
        );
        if (doInsert) insert con;
        return con;
    }

    public static Opportunity createOpportunity(Id accountId, String stage, Decimal amount, Boolean doInsert) {
        Opportunity opp = new Opportunity(
            Name = 'Test Opportunity',
            AccountId = accountId,
            StageName = stage,
            Amount = amount,
            CloseDate = Date.today().addDays(30)
        );
        if (doInsert) insert opp;
        return opp;
    }

    public static List<Opportunity> createOpportunities(Id accountId, Integer count, Boolean doInsert) {
        List<Opportunity> opps = new List<Opportunity>();
        for (Integer i = 0; i < count; i++) {
            opps.add(new Opportunity(
                Name = 'Test Opp ' + i,
                AccountId = accountId,
                StageName = 'Prospecting',
                Amount = 10000 + (i * 1000),
                CloseDate = Date.today().addDays(30 + i)
            ));
        }
        if (doInsert) insert opps;
        return opps;
    }

    public static User createUser(String profileName, Boolean doInsert) {
        Profile p = [SELECT Id FROM Profile WHERE Name = :profileName LIMIT 1];
        String uniqueKey = String.valueOf(DateTime.now().getTime());
        User u = new User(
            FirstName = 'Test',
            LastName = 'User ' + uniqueKey,
            Email = 'test' + uniqueKey + '@test.com',
            Username = 'test' + uniqueKey + '@test.com.sandbox',
            Alias = 'tst' + uniqueKey.right(4),
            ProfileId = p.Id,
            TimeZoneSidKey = 'America/Los_Angeles',
            LocaleSidKey = 'en_US',
            EmailEncodingKey = 'UTF-8',
            LanguageLocaleKey = 'en_US'
        );
        if (doInsert) insert u;
        return u;
    }
}

Key Techniques

1. Testing Triggers with Bulk Data

@isTest
static void testOpportunityClosedWonCreatesRenewal() {
    // Setup
    Account acct = TestDataFactory.createAccount('Acme Corp', true);
    List<Opportunity> opps = TestDataFactory.createOpportunities(acct.Id, 200, true);

    // Execute
    Test.startTest();
    for (Opportunity opp : opps) {
        opp.StageName = 'Closed Won';
    }
    update opps;
    Test.stopTest();

    // Verify
    List<Opportunity> renewals = [
        SELECT Id, Name, Type, OriginalOpportunity__c
        FROM Opportunity
        WHERE Type = 'Renewal'
    ];
    System.assertEquals(200, renewals.size(), 'Should create one renewal per closed won opp');

    for (Opportunity renewal : renewals) {
        System.assertNotEquals(null, renewal.OriginalOpportunity__c, 'Renewal should link to original');
        System.assert(renewal.Name.contains('Renewal'), 'Renewal name should contain Renewal');
    }
}

@isTest
static void testOpportunityClosedLostNoRenewal() {
    Account acct = TestDataFactory.createAccount('Acme Corp', true);
    Opportunity opp = TestDataFactory.createOpportunity(acct.Id, 'Prospecting', 50000, true);

    Test.startTest();
    opp.StageName = 'Closed Lost';
    opp.LossReason__c = 'Price';
    update opp;
    Test.stopTest();

    List<Opportunity> renewals = [SELECT Id FROM Opportunity WHERE Type = 'Renewal'];
    System.assertEquals(0, renewals.size(), 'Closed Lost should not create renewal');
}

2. Testing Batch Apex

@isTest
static void testAccountHealthScoreBatch() {
    // Setup: Create accounts with various health signals
    List<Account> accounts = TestDataFactory.createAccounts(50, true);

    // Create won opportunities for some accounts
    List<Opportunity> opps = new List<Opportunity>();
    for (Integer i = 0; i < 25; i++) {
        opps.add(new Opportunity(
            Name = 'Won Deal ' + i,
            AccountId = accounts[i].Id,
            StageName = 'Closed Won',
            Amount = 150000,
            CloseDate = Date.today()
        ));
    }
    insert opps;

    // Execute batch
    Test.startTest();
    Database.executeBatch(new AccountHealthScoreBatch(), 200);
    Test.stopTest();

    // Verify
    List<Account> updated = [SELECT Id, HealthScore__c FROM Account WHERE Id IN :accounts];
    Integer scoredAccounts = 0;
    for (Account a : updated) {
        if (a.HealthScore__c != null) scoredAccounts++;
    }
    System.assertEquals(50, scoredAccounts, 'All accounts should have health scores');

    // Verify accounts with won opps have higher scores
    Account withOpps = [SELECT HealthScore__c FROM Account WHERE Id = :accounts[0].Id];
    Account withoutOpps = [SELECT HealthScore__c FROM Account WHERE Id = :accounts[49].Id];
    System.assert(withOpps.HealthScore__c > withoutOpps.HealthScore__c, 'Account with won deals should score higher');
}

3. Mocking HTTP Callouts

@isTest
public class ERPSyncServiceTest {

    private class MockERPSuccess implements HttpCalloutMock {
        public HTTPResponse respond(HTTPRequest req) {
            HttpResponse res = new HttpResponse();
            res.setStatusCode(200);
            res.setHeader('Content-Type', 'application/json');
            res.setBody('{"id": "ERP-12345", "status": "created"}');
            return res;
        }
    }

    private class MockERPFailure implements HttpCalloutMock {
        public HTTPResponse respond(HTTPRequest req) {
            HttpResponse res = new HttpResponse();
            res.setStatusCode(500);
            res.setBody('{"error": "Internal Server Error"}');
            return res;
        }
    }

    @isTest
    static void testSyncSuccess() {
        Account acct = TestDataFactory.createAccount('Test', true);
        acct.ERP_Id__c = 'ERP-ACCT-001';
        update acct;

        Order__c order = new Order__c(Name = 'Test Order', Account__c = acct.Id, Amount__c = 50000);
        insert order;

        Test.setMock(HttpCalloutMock.class, new MockERPSuccess());
        Test.startTest();
        ERPSyncService.syncOrderToERP(order.Id);
        Test.stopTest();

        Order__c updated = [SELECT ERP_Order_Id__c, Sync_Status__c FROM Order__c WHERE Id = :order.Id];
        System.assertEquals('ERP-12345', updated.ERP_Order_Id__c);
        System.assertEquals('Synced', updated.Sync_Status__c);
    }

    @isTest
    static void testSyncFailure() {
        Account acct = TestDataFactory.createAccount('Test', true);
        Order__c order = new Order__c(Name = 'Test Order', Account__c = acct.Id, Amount__c = 50000);
        insert order;

        Test.setMock(HttpCalloutMock.class, new MockERPFailure());
        Test.startTest();
        ERPSyncService.syncOrderToERP(order.Id);
        Test.stopTest();

        Order__c updated = [SELECT Sync_Status__c, Sync_Error__c FROM Order__c WHERE Id = :order.Id];
        System.assertEquals('Error', updated.Sync_Status__c);
        System.assert(updated.Sync_Error__c.contains('Internal Server Error'));
    }
}

4. Testing with System.runAs

@isTest
static void testFieldLevelSecurity() {
    // Create user with limited profile
    User salesUser = TestDataFactory.createUser('Standard User', true);

    Account acct = TestDataFactory.createAccount('Test', true);
    Opportunity opp = TestDataFactory.createOpportunity(acct.Id, 'Prospecting', 100000, true);

    System.runAs(salesUser) {
        Test.startTest();
        // Verify sales user can read Amount but not Cost__c
        Opportunity queried = [SELECT Id, Amount FROM Opportunity WHERE Id = :opp.Id];
        System.assertNotEquals(null, queried.Amount);

        // Verify DML as this user
        queried.StageName = 'Discovery';
        queried.Description = 'Updated by sales user';
        queried.NextStep = 'Schedule demo';
        update queried;
        Test.stopTest();

        Opportunity updated = [SELECT StageName, Description FROM Opportunity WHERE Id = :opp.Id];
        System.assertEquals('Discovery', updated.StageName);
    }
}

Best Practices

  • Assert everything: Every test method must have at least one System.assert or System.assertEquals
  • Test bulk: Insert 200+ records to verify trigger bulkification
  • Use Test.startTest/stopTest: Reset governor limits for the code under test
  • No SeeAllData: Never use @isTest(SeeAllData=true) unless testing against org config
  • Test positive and negative: Verify both success paths and error/validation paths
  • Isolate tests: Each test method should create its own data, never depend on other test methods
  • Mock external callouts: Use HttpCalloutMock for every callout test
  • Test as different users: Use System.runAs to verify security and sharing

Common Pitfalls

  • No assertions: Running code without checking results passes coverage but verifies nothing
  • Hardcoded IDs: Using record type IDs from production that do not exist in test context
  • Order-dependent tests: Test B fails when Test A does not run first
  • SeeAllData dependency: Tests that rely on existing data break in empty sandboxes
  • Testing framework instead of logic: Testing that DML works instead of testing business rules

Anti-Patterns

  • Coverage Theater: Test methods that call every line but assert nothing. The code could return garbage and tests would pass.
  • The Monolith Test: One 500-line test method that tests everything. Break into focused test methods.
  • Copy-Paste Test Data: Creating the same Account/Contact/Opportunity setup in every test class. Use TestDataFactory.
  • Testing the Platform: Verifying that insert works or that a formula field calculates. Test YOUR logic, not Salesforce's.

Install this skill directly: skilldb add salesforce-skills

Get CLI access →