For org-wide testing policies (coverage requirements, 1:1 test method relationship), see Testing Policy.

Apex Testing Standards

This section defines core test conventions. Detailed implementation guidance is grouped later under Test Data Management, Assertion Best Practices, and Test Organization and Structure.

Project code prefix

Apex test class names use the same ALL_CAPS Apex prefix as production code, with a Test suffix (for example ACME_AccountServiceTest). How your team chooses and records that prefix is covered in Project code prefix.

Test Class Naming Conventions

  • Format: <APEX_PREFIX>_ClassNameTest (placeholder: ACME_…Test)
  • Pattern: Production class name followed by Test suffix
  • Examples:
// Production class: ACME_AccountService
// Test class: ACME_AccountServiceTest

// Production class: ACME_AccountSelector
// Test class: ACME_AccountSelectorTest

Test Method Naming Conventions

  • Format: test_<methodToTest> for primary test method, test_<methodToTest>_<qualifier> for additional paths
  • Pattern:
    • Primary test method covers the majority/happy path: test_<methodToTest>
    • Additional test methods cover edge cases, error scenarios, or alternative paths: test_<methodToTest>_<qualifier>
  • Examples:
@IsTest
private static void test_createAccount() {
    // Primary test method - covers the main/happy path for createAccount
}

@IsTest
private static void test_createAccount_nullName() {
    // Additional test method - tests null name scenario
}

@IsTest
private static void test_createAccount_emptyName() {
    // Additional test method - tests empty name scenario
}

@IsTest
private static void test_getAccountsByType() {
    // Primary test method - covers the main path for getAccountsByType
}

@IsTest
private static void test_getAccountsByType_nullType() {
    // Additional test method - tests null type scenario
}

@IsTest
private static void test_updateAccount() {
    // Primary test method - covers the main path for updateAccount
}

@IsTest
private static void test_updateAccount_invalidId() {
    // Additional test method - tests invalid ID scenario
}

@IsTest Annotation

  • All test methods must use @IsTest annotation
  • Test methods should be private static
  • Use @IsTest(seeAllData=true) only when absolutely necessary (see Test Data Management section)

Example:

@IsTest
private static class ACME_AccountServiceTest {
    @IsTest
    private static void test_createAccount() {
        // Primary test - covers main path for createAccount
    }
}

@IsTest(seeAllData=true) Prohibition

@IsTest(seeAllData=true) is not allowed without reasoning and documentation.

If you must use seeAllData=true, you must:

  1. Document why it's necessary
  2. Explain why test data creation isn't sufficient
  3. Get approval from the team lead

Example:

/**
 * @description Test class for Organization settings
 * @seeAllData true - Required to test Organization-level settings that cannot be created in tests
 */
@IsTest(seeAllData=true)
public class ACME_OrganizationSettingsTest {
    // Tests
}

@TestVisible Usage Guidelines

Only use @TestVisible on private methods for 1:1 testing standard.

This allows test classes to access private methods when necessary to achieve 1:1 test method to code method relationship.

Example:

public class ACME_AccountSelector {
    @TestVisible
    private String buildFieldList(Set<String> fields) {
        // Private method accessible to tests
    }
}

Test Method Structure

  • Use descriptive test method names
  • Follow naming convention: test_<methodToTest> for primary coverage and test_<methodToTest>_<qualifier> for additional paths
  • Use @IsTest annotation
  • Include assertions with descriptive messages

Example:

@IsTest
private static void createAccount_validName_createsAccount() {
    // Arrange
    String accountName = 'Test Account';

    // Act
    Account acc = ACME_AccountService.createAccount(accountName);

    // Assert
    System.assertNotEquals(null, acc.Id, 'Account should be created with an ID');
    System.assertEquals(accountName, acc.Name, 'Account name should match');
}

@TestSetup Patterns

Use @TestSetup for shared, stable test data. Keep setup focused on data creation only and avoid assertions in setup.

For detailed patterns and expanded examples, see Test Data Management@TestSetup Best Practices.

Quick example:

@IsTest
private static class ACME_AccountServiceTest {
    @TestSetup
    static void testSetup() {
        List<Account> testAccounts = new List<Account>();
        for (Integer i = 1; i <= 10; i++) {
            testAccounts.add(new Account(Name = 'Test Account ' + i));
        }
        insert testAccounts;
    }

    @IsTest
    private static void test_getAccounts() {
        // Arrange - Create test user with appropriate permissions
        User testUser = TestFactory.createUser('Standard User');

        // Act - Run test as test user
        System.runAs(testUser) {
            // Can use accounts created in @TestSetup
            List<Account> accounts = ACME_AccountService.getAccounts();
            Assert.areEqual(10, accounts.size(), 'Should return 10 accounts');
        }
    }
}

Test User Requirements

All tests must run with a test user, never as the current user.

Requirements:

  • Always use System.runAs() with a test user for all test execution
  • Never run tests as the current user - tests must be isolated and predictable
  • Create test users with appropriate permissions for each specific test scenario
  • Use test data factories (e.g., TestFactory.createUser()) to create test users
  • Assign permission sets or profiles as needed for the test scenario

Example:

@IsTest
private static void test_createAccount() {
    // Arrange - Create test user with appropriate permissions
    User testUser = TestFactory.createUser('Standard User');
    String accountName = 'Test Account';

    // Act - Run test as test user
    System.runAs(testUser) {
        Account acc = ACME_AccountService.createAccount(accountName);

        // Assert
        Assert.isNotNull(acc.Id, 'Account should be created with an ID');
        Assert.areEqual(accountName, acc.Name, 'Account name should match');
    }
}

Bad Example (DO NOT DO THIS):

@IsTest
private static void test_createAccount() {
    // BAD - Running as current user
    String accountName = 'Test Account';
    Account acc = ACME_AccountService.createAccount(accountName);
    Assert.isNotNull(acc.Id, 'Account should be created with an ID');
}

Test Data Creation Patterns

  • Use test data factories when available (e.g., TestFactory, TestUtils)
  • Create minimal data necessary for each scenario
  • Use meaningful test data names
  • Avoid hardcoded IDs - use queries or variables
  • Always create and use test users - never run tests as current user

For reusable factory patterns, see Test Data ManagementTest Data Factory Patterns.

Quick example:

@IsTest
private static void test_createAccount() {
    // Arrange - Create test user with appropriate permissions
    User testUser = TestFactory.createUser('Standard User');
    String accountName = 'Test Account';

    // Act - Run test as test user
    System.runAs(testUser) {
        Account acc = ACME_AccountService.createAccount(accountName);

        // Assert
        Assert.isNotNull(acc.Id, 'Account should be created with an ID');
        Assert.areEqual(accountName, acc.Name, 'Account name should match');
    }
}

Test Isolation and Cleanup

  • Tests must be independent - no test should depend on another
  • Each test should set up its own data (or use @TestSetup)
  • Use Test.startTest() and Test.stopTest() around the behavior under test
  • Prefer transaction rollback isolation over manual cleanup when possible

For broader isolation guidance, see Test Data ManagementTest Data Isolation.

Quick example:

@IsTest
private static void test_processRecords() {
    // Arrange - Create test user with appropriate permissions
    User testUser = TestFactory.createUser('Standard User');
    List<Account> accounts = new List<Account>();
    for (Integer i = 0; i < 200; i++) {
        accounts.add(new Account(Name = 'Test ' + i));
    }
    insert accounts;

    // Act - Run test as test user
    System.runAs(testUser) {
        Test.startTest();
        ACME_AccountService.processRecords(accounts);
        Test.stopTest();

        // Assert
        // Verify results
    }
}

Bulk Testing Requirements

All methods that process collections must be tested with bulk data (200+ records).

This ensures code is bulkified and handles governor limits correctly.

Example:

@IsTest
private static void test_updateAccounts() {
    // Arrange - Create test user with appropriate permissions
    User testUser = TestFactory.createUser('Standard User');
    // Create 200+ records
    List<Account> accounts = new List<Account>();
    for (Integer i = 0; i < 200; i++) {
        accounts.add(new Account(Name = 'Test ' + i));
    }
    insert accounts;

    // Modify accounts
    for (Account acc : accounts) {
        acc.Name = 'Updated ' + acc.Name;
    }

    // Act - Run test as test user
    System.runAs(testUser) {
        Test.startTest();
        ACME_AccountService.updateAccounts(accounts);
        Test.stopTest();

        // Assert
        List<Account> updatedAccounts = [SELECT Name FROM Account WHERE Id IN :accounts];
        Assert.areEqual(200, updatedAccounts.size(), 'All 200 accounts should be updated');
    }
}

Error Scenario Testing

Test successful execution paths first. Exception testing (try-catch blocks) is optional and only required if needed to reach the 90% coverage threshold.

Important Distinction:

  • Exception Testing (try-catch): Optional - only if needed for coverage
  • Conditional Logic (if/else, switch): Required - must test all branches

Priority:

  1. Required: Test successful execution paths (happy path)
  2. Required: Test all conditional logic branches (if/else true and false, all switch cases)
  3. Required: Test edge cases that affect business logic (empty lists, boundary values)
  4. Required: Test security violations (CRUD/FLS) when security is a concern
  5. Optional: Test exception paths (try-catch blocks) - only if needed to reach 90% coverage threshold

Guidelines:

  • Don't force exception testing (try-catch) if you don't need it for coverage
  • Must test all conditional branches - if statements require both true and false paths
  • Must test all switch cases - all switch statement branches must be covered
  • Focus on testing the code that matters for business logic
  • Exception testing (try-catch) should be a last resort to meet coverage requirements, not a requirement for every method

For deeper examples of branch and positive/negative coverage, see Assertion Best PracticesTesting Positive and Negative Cases.

Example:

@IsTest
private static void test_createAccount() {
    // Primary test - positive test case covering main path (REQUIRED)
}

@IsTest
private static void test_createAccount_emptyList() {
    // Additional test - edge case that affects business logic (REQUIRED if relevant)
}

// Exception testing (try-catch) - ONLY if needed to reach 90% coverage threshold
@IsTest
private static void test_createAccount_nullName() {
    // Optional - only add if needed for coverage
    try {
        ACME_AccountService.createAccount(null);
        Assert.fail('Should have thrown exception for null name');
    } catch (IllegalArgumentException e) {
        Assert.isTrue(true, 'Exception thrown as expected');
    }
}

Security Testing (CRUD/FLS)

Test that code properly enforces CRUD and FLS security.

  • Test with users who don't have access
  • Test with users who have partial access
  • Verify Security.stripInaccessible() is used correctly
  • Test sharing model enforcement

Example:

@IsTest
private static void test_getAccounts_userWithoutAccess() {
    // Arrange
    User testUser = TestFactory.createUserWithoutPermissions();
    List<Account> accounts = TestFactory.createAccounts(5);

    // Act
    System.runAs(testUser) {
        List<Account> result = ACME_AccountService.getAccounts();

        // Assert
        Assert.areEqual(0, result.size(), 'User without access should see no accounts');
    }
}

Performance Testing Considerations

  • Test governor limit usage
  • Test with large datasets
  • Verify queries are optimized
  • Test DML bulkification

Example:

@IsTest
private static void test_processRecords_largeDataset() {
    // Arrange - Create test user with appropriate permissions
    User testUser = TestFactory.createUser('Standard User');
    List<Account> accounts = new List<Account>();
    for (Integer i = 0; i < 1000; i++) {
        accounts.add(new Account(Name = 'Test ' + i));
    }

    // Act - Run test as test user
    System.runAs(testUser) {
        Test.startTest();
        ACME_AccountService.processRecords(accounts);
        Test.stopTest();

        // Assert - Verify no governor limit exceptions
        Assert.isTrue(Limits.getQueries() < Limits.getLimitQueries(), 'Should not exceed query limit');
    }
}

Test Data Management

Test Data Factory Patterns

Usage: Use test data factories to create consistent test data.

Guidelines:

  • Create reusable factory methods for common objects
  • Use builder pattern for complex test data
  • Support bulk data creation
  • Provide sensible defaults

Example:

@IsTest
public class TestFactory {
    public static Account createAccount() {
        return createAccount('Test Account', null);
    }

    public static Account createAccount(String name) {
        return createAccount(name, null);
    }

    public static Account createAccount(String name, Map<String, Object> fieldValues) {
        Account acc = new Account(Name = name);
        if (fieldValues != null) {
            for (String field : fieldValues.keySet()) {
                acc.put(field, fieldValues.get(field));
            }
        }
        return acc;
    }

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

@TestSetup Best Practices

Usage: Use @TestSetup for shared test data.

Guidelines:

  • Create common data in @TestSetup
  • Keep setup focused on data creation
  • Don't perform assertions in setup
  • Use setup for data that doesn't vary between tests

Example:

@IsTest
private static class ACME_AccountServiceTest {
    @TestSetup
    static void testSetup() {
        // Create common test data
        List<Account> accounts = TestFactory.createAccounts(10);
        insert accounts;

        List<Contact> contacts = TestFactory.createContacts(5, accounts);
        insert contacts;
    }

    @IsTest
    private static void test_getAccounts() {
        // Arrange - Create test user with appropriate permissions
        User testUser = TestFactory.createUser('Standard User');

        // Act - Run test as test user
        System.runAs(testUser) {
            // Can use accounts from @TestSetup
            List<Account> accounts = ACME_AccountService.getAccounts();
            Assert.areEqual(10, accounts.size(), 'Should return accounts from setup');
        }
    }
}

Test Data Isolation

Guidelines:

  • Each test should be independent
  • Tests should not depend on execution order
  • Use @TestSetup or create data in each test
  • Clean up data when necessary

Avoiding seeAllData=true

@IsTest(seeAllData=true) is prohibited unless absolutely necessary.

Requirements if you must use it (same policy as earlier section):

  1. Document why it's necessary
  2. Explain why test data creation isn't sufficient
  3. Get approval from team lead

Assertion Best Practices

Descriptive Assertion Messages

All assertions must include descriptive messages.

Format: Assert.areEqual(expected, actual, message)

Message Guidelines:

  • Explain what is being tested
  • Include context (method name, input values)
  • Explain why the assertion matters
  • Be specific about expected vs. actual values

Examples:

// Good: Descriptive message using Assert class
Assert.areEqual(10, accounts.size(), 'getAccounts should return 10 accounts from test setup');

// Good: Includes context
Assert.areEqual('Test Account', acc.Name, 'createAccount should set account name correctly');

// Bad: No message
Assert.areEqual(10, accounts.size());

// Bad: Vague message
Assert.areEqual(10, accounts.size(), 'Failed');

Appropriate Assertion Methods

Use the most appropriate Assert class method:

  • Assert.areEqual(expected, actual, message) - For equality checks
  • Assert.areNotEqual(unexpected, actual, message) - For inequality checks
  • Assert.isTrue(condition, message) - For true conditions
  • Assert.isFalse(condition, message) - For false conditions
  • Assert.isNotNull(actual, message) - For null checks (not null)
  • Assert.isNull(actual, message) - For null checks (is null)
  • Assert.fail(message) - Force test failure with message

Examples:

// Equality check
Assert.areEqual(expectedValue, actualValue, 'Values should match');

// Inequality check
Assert.areNotEqual(null, account.Id, 'Account should have an ID');

// Boolean condition - true
Assert.isTrue(accounts.size() > 0, 'Should have at least one account');

// Boolean condition - false
Assert.isFalse(accounts.isEmpty(), 'Should not be empty');

// Null check - not null
Assert.isNotNull(result, 'Result should not be null');

// Null check - is null
Assert.isNull(result, 'Result should be null');

Testing Positive and Negative Cases

Focus on testing successful execution paths. Exception testing (try-catch) is optional, but conditional logic (if/else, switch) must test all branches.

Important Distinction:

  • Exception Testing (try-catch blocks): Optional - only if needed for coverage
  • Conditional Logic (if/else statements): Required - must test both true and false paths
  • Switch Statements: Required - must test all cases

Priority:

  1. Required: Test successful execution paths (happy path)
  2. Required: Test all conditional branches (if/else true and false, all switch cases)
  3. Optional: Test exception scenarios (try-catch blocks) - only if needed for coverage

Example:

@IsTest
private static void test_createAccount() {
    // Arrange - Create test user with appropriate permissions
    User testUser = TestFactory.createUser('Standard User');

    // Act - Run test as test user
    System.runAs(testUser) {
        // Primary test - positive test case covering main path (REQUIRED)
        Account acc = ACME_AccountService.createAccount('Test Account');
        Assert.isNotNull(acc.Id, 'Account should be created');
    }
}

// Conditional logic testing - REQUIRED (if method has if/else or switch)
@IsTest
private static void test_updateAccount_withValidStatus() {
    // Arrange - Create test user with appropriate permissions
    User testUser = TestFactory.createUser('Standard User');
    Account acc = new Account(Name = 'Test', Status__c = 'Active');
    insert acc;

    // Act - Run test as test user
    System.runAs(testUser) {
        // Required - test if statement true path
        Account updated = ACME_AccountService.updateAccount(acc);
        Assert.areEqual('Active', updated.Status__c, 'Status should remain Active');
    }
}

@IsTest
private static void test_updateAccount_withInvalidStatus() {
    // Arrange - Create test user with appropriate permissions
    User testUser = TestFactory.createUser('Standard User');
    Account acc = new Account(Name = 'Test', Status__c = 'Invalid');
    insert acc;

    // Act - Run test as test user
    System.runAs(testUser) {
        // Required - test if statement false path
        Account updated = ACME_AccountService.updateAccount(acc);
        Assert.areEqual('Pending', updated.Status__c, 'Invalid status should default to Pending');
    }
}

// Exception testing (try-catch) - ONLY if needed to reach 90% coverage threshold
@IsTest
private static void test_createAccount_nullNameThrowsException() {
    // Arrange - Create test user with appropriate permissions
    User testUser = TestFactory.createUser('Standard User');

    // Act - Run test as test user
    System.runAs(testUser) {
        // Optional - only add if needed for coverage
        try {
            ACME_AccountService.createAccount(null);
            Assert.fail('Should have thrown exception for null name');
        } catch (IllegalArgumentException e) {
            Assert.isTrue(true, 'Exception thrown as expected: ' + e.getMessage());
        }
    }
}

Edge Case Testing

Test boundary conditions and edge cases that affect business logic.

Examples:

@IsTest
private static void test_processRecords_emptyList() {
    List<Account> emptyList = new List<Account>();
    ACME_Response response = ACME_AccountService.processRecords(emptyList);
    Assert.areEqual(0, response.results.size(), 'Empty list should return empty response');
}

@IsTest
private static void test_createAccount_maxLengthName() {
    String maxLengthName = 'A'.repeat(255);
    Account acc = ACME_AccountService.createAccount(maxLengthName);
    Assert.areEqual(maxLengthName, acc.Name, 'Should handle maximum length name');
}

Mocking Strategies

Apex Mocking (Test.setMock)

Usage: Mock HTTP callouts and platform events.

Example:

@IsTest
private static void test_makeCallout() {
    // Arrange - Create test user with appropriate permissions
    User testUser = TestFactory.createUser('Standard User');

    // Act - Run test as test user
    System.runAs(testUser) {
        // Primary test - mock HTTP callout
        Test.setMock(HttpCalloutMock.class, new MockHttpResponseGenerator());

        Test.startTest();
        HttpResponse response = ACME_IntegrationService.makeCallout();
        Test.stopTest();

        Assert.areEqual(200, response.getStatusCode(), 'Should return successful response');
    }
}

Test Organization and Structure

Test Class Organization

Structure:

  1. Class-level documentation
  2. @TestSetup method (if needed)
  3. Helper methods (if needed)
  4. Test methods (grouped by functionality)

Example:

/**
 * @description Test class for ACME_AccountService
 * @author contributor1
 */
@IsTest
private static class ACME_AccountServiceTest {
    @TestSetup
    static void testSetup() {
        // Setup common test data
    }

    // Helper methods
    private static Account createTestAccount() {
        return new Account(Name = 'Test Account');
    }

    // Test methods grouped by functionality
    // Create methods
    @IsTest
    private static void test_createAccount() {
        // Primary test - covers main path for createAccount
    }

    // Read methods
    @IsTest
    private static void test_getAccounts() {
        // Primary test - covers main path for getAccounts
    }

    // Update methods
    @IsTest
    private static void test_updateAccount() {
        // Primary test - covers main path for updateAccount
    }
}

Test Method Organization

Use Arrange-Act-Assert pattern:

@IsTest
private static void test_createAccount() {
    // Arrange - Create test user with appropriate permissions and set up test data
    User testUser = TestFactory.createUser('Standard User');
    String accountName = 'Test Account';

    // Act - Run test as test user and execute the method being tested
    System.runAs(testUser) {
        Account acc = ACME_AccountService.createAccount(accountName);

        // Assert - Verify the results
        Assert.isNotNull(acc.Id, 'Account should be created with an ID');
        Assert.areEqual(accountName, acc.Name, 'Account name should match');
    }
}

Helper Method Patterns

Use helper methods for:

  • Common test data creation
  • Complex assertions
  • Repeated test setup
  • Test utilities

Test Utilities Usage

Use test utility classes for:

  • Common test data factories
  • Assertion helpers
  • Mock data generators
  • Test configuration

Code Analyzer Compliance

Key rules for test classes:

  • ApexAssertionsShouldIncludeMessage - All assertions must include messages
  • ApexUnitTestClassShouldHaveAsserts - Test classes must have at least one assertion
  • ApexUnitTestMethodShouldHaveIsTestAnnotation - Test methods must use @IsTest
  • ApexUnitTestShouldNotUseSeeAllDataTrue - Avoid seeAllData=true

Common Violations and How to Avoid Them

Violation: Missing assertion messages

// Bad
Assert.areEqual(expected, actual);

// Good
Assert.areEqual(expected, actual, 'Descriptive message explaining what is being tested');

Violation: Test class without assertions

// Bad
@IsTest
private static class ACME_AccountServiceTest {
    @IsTest
    private static void test_createAccount() {
        ACME_AccountService.createAccount('Test Account');
        // No assertions
    }
}

// Good
@IsTest
private static class ACME_AccountServiceTest {
    @IsTest
    private static void test_createAccount() {
        Account acc = ACME_AccountService.createAccount('Test Account');
        Assert.isNotNull(acc.Id, 'Account should be created');
    }
}

Violation: Missing @IsTest annotation

// Bad
private static void test_createAccount() {
    // Missing @IsTest
}

// Good
@IsTest
private static void test_createAccount() {
    // Has @IsTest annotation
}

Code Analyzer Validation

  • Run Code Analyzer before committing: sf code-analyzer run --target "**/*Test.cls"
  • Fix all Critical and High severity violations
  • Review Medium and Low severity violations
  • Ensure zero violations before PR approval

Change History

Version 1.0 - 2026-03-26

  • Added initial version history tracking for this document.

Last Updated: 2026-03-26 Version: 1.0