Author: callinj
Name: Joe Callin
Title: Sr. Salesforce Developer
Email: joe@jcallin.dev

Table of Contents

  1. Introduction
  2. Test Coverage Requirements
  3. 1:1 Test Method to Code Method Relationship
  4. Apex Testing Standards
  5. Test Data Management
  6. Assertion Best Practices
  7. Mocking Strategies
  8. Test Organization and Structure
  9. Code Analyzer Compliance
  10. LWC Testing Standards

Introduction

Purpose

This document establishes comprehensive testing standards and best practices for both Apex and Lightning Web Component (LWC) testing within the Salesforce component library. These standards ensure consistent, maintainable, and high-quality test coverage across all code.

Scope

These standards apply to all test classes, test methods, and test utilities within the component library. All tests must comply with these standards and pass Salesforce Code Analyzer validation using the project's ruleset.

Test Coverage Requirements

Coverage Goals

Target: 100% code coverage for all production code.

Minimum: 90% code coverage required. Any code below 90% coverage must have documented reasoning explaining why full coverage is not feasible or necessary.

Coverage Reporting

  • Coverage must be verified before code is committed
  • Use Salesforce CLI to generate coverage reports: sf apex run test --code-coverage
  • Coverage reports should be reviewed as part of code review process
  • Coverage gaps must be addressed or documented

Exclusions and Exceptions

Code that may be excluded from coverage requirements (with documentation):

  • Getter/Setter methods - Simple property accessors may be excluded if they don't contain business logic
  • Exception constructors - Custom exception classes with only constructors
  • Test utilities - Test factory and utility classes themselves (meta-testing)
  • Deprecated code - Code marked for removal (must have removal plan)

Documentation Format:

/**
 * @description Simple getter method for account name
 * @coverage-excluded - Simple property accessor with no business logic
 */
public String getAccountName() {
    return this.accountName;
}

Coverage Validation

  • All test classes must be run before committing code
  • Coverage must meet minimum requirements before PR approval
  • CI/CD pipelines should validate coverage automatically
  • Coverage reports should be included in PR reviews

1:1 Test Method to Code Method Relationship

Requirement

Each public and protected method in production code should have a corresponding test method.

This ensures comprehensive test coverage and makes it easy to identify which tests cover which methods.

Implementation

  • One test method per production method
  • Test method names should clearly indicate which method they test
  • Test methods should focus on testing a single method's behavior

Example:

// Production code
public class SFC_AccountService {
    public static Account createAccount(String accountName) {
        // Implementation
    }

    public static List<Account> getAccountsByType(String type) {
        // Implementation
    }
}

// Test code
@isTest
private static class SFC_AccountServiceTest {
    @isTest
    private static void test_createAccount() {
        // Tests createAccount method - primary test covering main path
    }

    @isTest
    private static void test_getAccountsByType() {
        // Tests getAccountsByType method - primary test covering main path
    }
}

Exceptions

Exceptions to the 1:1 rule are acceptable in these cases:

  1. Private methods - May be tested indirectly through public methods, or use @TestVisible if direct testing is necessary
  2. Overloaded methods - Each overload should have its own test method
  3. Complex methods - Methods with multiple distinct behaviors may have multiple test methods (one per behavior)
  4. Helper methods - Utility methods may be tested as part of calling method tests if they're simple

Example of multiple tests for complex method:

// Production code
public static ResponseService processRecords(List<SObject> records) {
    // Complex method with multiple behaviors
}

// Test code - Multiple tests for different behaviors
@isTest
private static void test_processRecords() {
    // Primary test - tests successful processing (main path)
}

@isTest
private static void test_processRecords_emptyList() {
    // Additional test - tests empty list handling
}

@isTest
private static void test_processRecords_invalidRecords() {
    // Additional test - tests error handling
}

@TestVisible Usage

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 SFC_QueryBuilderService {
    @TestVisible
    private String buildQueryString(String type, String field) {
        // Private method accessible to tests
    }
}

@isTest
private static class SFC_QueryBuilderServiceTest {
    @isTest
    private static void test_buildQueryString() {
        // Primary test - can directly test private method
    }
}

Apex Testing Standards

Test Class Naming Conventions

  • Format: SFC_ClassNameTest
  • Pattern: Production class name followed by Test suffix
  • Examples:
// Production class: SFC_AccountService
// Test class: SFC_AccountServiceTest

// Production class: SFC_QueryBuilderService
// Test class: SFC_QueryBuilderServiceTest

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 SFC_AccountServiceTest {
    @isTest
    private static void test_createAccount() {
        // Primary test - covers main path for createAccount
    }
}

@TestSetup Patterns

Usage: Use @TestSetup to create test data that is shared across multiple test methods.

Guidelines:

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

Example:

@isTest
private static class SFC_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 = SFC_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 = SFC_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 = SFC_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 test
  • Use meaningful test data names
  • Avoid hardcoded IDs - use queries or variables
  • Always create and use test users - never run tests as current user

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 = SFC_AccountService.createAccount(accountName);

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

Assertion Best Practices

All assertions must include descriptive messages.

Use the System.Assert class for all assertions. The Assert class provides clearer, more readable assertion methods and was introduced in Winter '23.

  • Use Assert.areEqual(expected, actual, message) - For equality checks
  • Use Assert.areNotEqual(unexpected, actual, message) - For inequality checks
  • Use Assert.isTrue(condition, message) - For true conditions
  • Use Assert.isFalse(condition, message) - For false conditions
  • Use Assert.isNotNull(actual, message) - For null checks (not null)
  • Use Assert.isNull(actual, message) - For null checks (is null)
  • Messages should explain what is being tested and why it matters

Reference: System.Assert Class Documentation

Example:

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

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

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)
  • Tests should clean up after themselves when necessary
  • Use Test.startTest() and Test.stopTest() to reset governor limits

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();
        SFC_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();
        SFC_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

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 {
        SFC_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 = SFC_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();
        SFC_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 SFC_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 = SFC_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

Example:

@isTest
private static class SFC_AccountServiceTest {
    @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) {
            // This test creates its own data
            Account acc = SFC_AccountService.createAccount('Test Account');
            Assert.isNotNull(acc.Id, 'Account should be created');
        }
    }

    @isTest
    private static void test_updateAccount() {
        // Arrange - Create test user with appropriate permissions
        User testUser = TestFactory.createUser('Standard User');
        Account acc = new Account(Name = 'Original Name');
        insert acc;

        // Act - Run test as test user
        System.runAs(testUser) {
            acc.Name = 'Updated Name';
            SFC_AccountService.updateAccount(acc);

            // Assert
            Account updated = [SELECT Name FROM Account WHERE Id = :acc.Id];
            Assert.areEqual('Updated Name', updated.Name, 'Name should be updated');
        }
    }
}

Test Data Cleanup

Guidelines:

  • Salesforce automatically cleans up test data after test execution
  • For integration tests, may need explicit cleanup
  • Use @TestSetup to minimize data creation overhead

Avoiding seeAllData=true

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

Requirements if you must use it:

  1. Document why it's necessary
  2. Explain why test data creation isn't sufficient
  3. Get approval from 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 SFC_OrganizationSettingsTest {
    @isTest
    private static void test_getOrganizationSettings() {
        // Primary test - test implementation
    }
}

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

Guidelines:

  • Don't create exception tests (try-catch) just to have them - only if needed for coverage
  • Must test all if/else branches - both true and false conditions
  • Must test all switch cases - every case in a switch statement
  • Focus on testing business logic and successful paths
  • Exception testing (try-catch) should be a last resort to meet coverage requirements

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 = SFC_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 = SFC_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 = SFC_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 {
            SFC_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. Exception-related edge cases (try-catch blocks) are optional, but conditional logic branches (if/else, switch) must be tested.

Important Distinction:

  • Exception Testing (try-catch): Optional - only if needed for coverage
  • Conditional Logic (if/else, switch): Required - must test all branches
  • Null values in conditional logic: Required if null is handled in if/else or switch

Priority:

  1. Required: Edge cases that affect business logic (empty lists, boundary values, special characters)
  2. Required: All conditional logic branches (if/else true and false, all switch cases)
  3. Optional: Exception testing (try-catch blocks) - only if needed to reach 90% coverage threshold

Examples:

  • Empty lists/collections (if they affect business logic)
  • Maximum/minimum values (if they affect business logic)
  • Boundary conditions
  • Special characters in strings (if they affect business logic)
  • Null values in conditional logic (required if handled in if/else or switch)
  • Exception handling (try-catch blocks) - optional, only if needed for coverage
@isTest
private static void test_processRecords_emptyList() {
    // Additional test - edge case: empty list (REQUIRED if empty list handling affects business logic)
    List<Account> emptyList = new List<Account>();
    ResponseService response = SFC_AccountService.processRecords(emptyList);
    Assert.areEqual(0, response.results.size(), 'Empty list should return empty response');
}

@isTest
private static void test_createAccount_maxLengthName() {
    // Additional test - edge case: maximum length (REQUIRED if max length affects business logic)
    String maxLengthName = 'A'.repeat(255);
    Account acc = SFC_AccountService.createAccount(maxLengthName);
    Assert.areEqual(maxLengthName, acc.Name, 'Should handle maximum length name');
}

// Null value testing in conditional logic - REQUIRED if null is handled in if/else or switch
@isTest
private static void test_createAccount_nullName() {
    // Arrange - Create test user with appropriate permissions
    User testUser = TestFactory.createUser('Standard User');

    // Act - Run test as test user
    System.runAs(testUser) {
        // Required if method has: if (name == null) { ... } else { ... }
        Account acc = SFC_AccountService.createAccount(null);
        Assert.areEqual('Default Name', acc.Name, 'Null name should use default');
    }
}

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

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 = SFC_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 SFC_AccountService
 * @author callinj
 */
@isTest
private static class SFC_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 = SFC_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

Example:

@isTest
private static class SFC_AccountServiceTest {
    // Helper method for creating test accounts
    private static Account createTestAccount(String name) {
        return new Account(Name = name);
    }

    // Helper method for complex assertion
    private static void assertAccountEquals(Account expected, Account actual) {
        Assert.areEqual(expected.Name, actual.Name, 'Account names should match');
        Assert.areEqual(expected.Type, actual.Type, 'Account types should match');
    }

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

        // Act - Run test as test user
        System.runAs(testUser) {
            Account created = SFC_AccountService.createAccount(testAccount.Name);
            assertAccountEquals(testAccount, created);
        }
    }
}

Test Utilities Usage

Use test utility classes for:

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

Example:

@isTest
private static class SFC_AccountServiceTest {
    @isTest
    private static void test_createAccount() {
        // Arrange - Create test user with appropriate permissions
        User testUser = TestFactory.createUser('Standard User');
        Account testAccount = TestFactory.createAccount('Test Account');

        // Act - Run test as test user
        System.runAs(testUser) {
            // Primary test - use TestFactory for creating test data
            Account created = SFC_AccountService.createAccount(testAccount.Name);
            Assert.isNotNull(created.Id, 'Account should be created');
        }
    }
}

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

ApexAssertionsShouldIncludeMessage Compliance

Requirement: All assertions must include descriptive messages.

Example:

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

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

ApexUnitTestClassShouldHaveAsserts Compliance

Requirement: Every test class must have at least one assertion.

Example:

@isTest
private static class SFC_AccountServiceTest {
    @isTest
    private static void test_createAccount() {
        Account acc = SFC_AccountService.createAccount('Test Account');
        // Must have at least one assertion
        Assert.isNotNull(acc.Id, 'Account should be created with an ID');
    }
}

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 SFC_AccountServiceTest {
    @isTest
    private static void test_createAccount() {
        SFC_AccountService.createAccount('Test Account');
        // No assertions
    }
}

// Good
@isTest
private static class SFC_AccountServiceTest {
    @isTest
    private static void test_createAccount() {
        Account acc = SFC_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 analyze --target "**/*Test.cls"
  • Fix all Critical and High severity violations
  • Review Medium and Low severity violations
  • Ensure zero violations before PR approval

LWC Testing Standards

Note: LWC testing is currently a work in progress and not required as of right now. The standards outlined below are potential standards and serve as discussion points for future implementation. These guidelines may evolve as LWC testing practices mature and become a requirement.

Jest Testing Framework

Usage: Use Jest with @salesforce/sfdx-lwc-jest for LWC testing.

Setup:

  • Install dependencies: npm install --save-dev @salesforce/sfdx-lwc-jest
  • Configure Jest in jest.config.js
  • Run tests: npm test or sf lwc test

Test File Organization

  • Test files should be co-located with components
  • Test file naming: componentName.test.js
  • Test files should mirror component structure

Example Structure:

src/
└── components/
    └── lwc/
        └── sfcAction/
            ├── sfcAction.js
            ├── sfcAction.html
            ├── sfcAction.css
            ├── sfcAction.js-meta.xml
            └── __tests__/
                └── sfcAction.test.js

Component Testing Patterns

Basic Test Structure:

import { createElement } from 'lwc';
import SfcAction from 'c/sfcAction';

describe('c-sfc-action', () => {
    afterEach(() => {
        // Cleanup DOM after each test
        while (document.body.firstChild) {
            document.body.removeChild(document.body.firstChild);
        }
    });

    it('renders component successfully', () => {
        const element = createElement('c-sfc-action', {
            is: SfcAction
        });
        document.body.appendChild(element);

        return Promise.resolve().then(() => {
            // Assertions
            expect(element).not.toBeNull();
        });
    });
});

Mocking Wire Adapters

Usage: Mock wire adapters to control test data.

Example:

import { createElement } from 'lwc';
import AccountDetail from 'c/accountDetail';
import { getRecord } from 'lightning/uiRecordApi';

// Mock the wire adapter
jest.mock(
    'lightning/uiRecordApi',
    () => ({
        getRecord: jest.fn()
    }),
    { virtual: true }
);

describe('c-account-detail', () => {
    it('displays account name from wire adapter', () => {
        const mockRecord = {
            fields: {
                Name: { value: 'Test Account' }
            }
        };

        getRecord.mockResolvedValue(mockRecord);

        const element = createElement('c-account-detail', {
            is: AccountDetail
        });
        element.recordId = '001000000000000AAA';
        document.body.appendChild(element);

        return Promise.resolve().then(() => {
            const nameElement = element.shadowRoot.querySelector('.account-name');
            expect(nameElement.textContent).toBe('Test Account');
        });
    });
});

Mocking Apex Methods

Usage: Mock Apex method calls using jest.fn().

Example:

import { createElement } from 'lwc';
import AccountList from 'c/accountList';
import getAccounts from '@salesforce/apex/SFC_AccountService.getAccounts';

// Mock Apex method
jest.mock(
    '@salesforce/apex/SFC_AccountService.getAccounts',
    () => ({ default: jest.fn() }),
    { virtual: true }
);

describe('c-account-list', () => {
    it('displays accounts from Apex method', () => {
        const mockAccounts = [
            { Id: '001001', Name: 'Account 1' },
            { Id: '001002', Name: 'Account 2' }
        ];

        getAccounts.mockResolvedValue(mockAccounts);

        const element = createElement('c-account-list', {
            is: AccountList
        });
        document.body.appendChild(element);

        return Promise.resolve().then(() => {
            const accountElements = element.shadowRoot.querySelectorAll('.account-item');
            expect(accountElements.length).toBe(2);
        });
    });
});

Event Testing

Usage: Test component events (custom events and standard events).

Example:

import { createElement } from 'lwc';
import SfcAction from 'c/sfcAction';

describe('c-sfc-action', () => {
    it('dispatches actionclicked event on button click', () => {
        const element = createElement('c-sfc-action', {
            is: SfcAction
        });
        document.body.appendChild(element);

        const handler = jest.fn();
        element.addEventListener('actionclicked', handler);

        return Promise.resolve().then(() => {
            const button = element.shadowRoot.querySelector('button');
            button.click();

            expect(handler).toHaveBeenCalled();
            const event = handler.mock.calls[0][0];
            expect(event.detail).toBeDefined();
        });
    });
});

User Interaction Testing

Usage: Test user interactions (clicks, input, keyboard events).

Example:

import { createElement } from 'lwc';
import AccountForm from 'c/accountForm';

describe('c-account-form', () => {
    it('updates account name on input change', () => {
        const element = createElement('c-account-form', {
            is: AccountForm
        });
        document.body.appendChild(element);

        return Promise.resolve().then(() => {
            const input = element.shadowRoot.querySelector('lightning-input');
            input.value = 'New Account Name';
            input.dispatchEvent(new CustomEvent('change'));

            return Promise.resolve().then(() => {
                expect(element.accountName).toBe('New Account Name');
            });
        });
    });

    it('submits form on button click', () => {
        const element = createElement('c-account-form', {
            is: AccountForm
        });
        document.body.appendChild(element);

        const submitHandler = jest.fn();
        element.addEventListener('formsubmit', submitHandler);

        return Promise.resolve().then(() => {
            const submitButton = element.shadowRoot.querySelector('lightning-button');
            submitButton.click();

            expect(submitHandler).toHaveBeenCalled();
        });
    });
});

Accessibility Testing in Tests

Usage: Test accessibility features (ARIA labels, keyboard navigation).

Example:

import { createElement } from 'lwc';
import SfcAction from 'c/sfcAction';

describe('c-sfc-action', () => {
    it('has proper ARIA label', () => {
        const element = createElement('c-sfc-action', {
            is: SfcAction
        });
        element.label = 'Submit Form';
        element.ariaLabel = 'Submit form';
        document.body.appendChild(element);

        return Promise.resolve().then(() => {
            const button = element.shadowRoot.querySelector('button');
            expect(button.getAttribute('aria-label')).toBe('Submit form');
        });
    });

    it('is keyboard accessible', () => {
        const element = createElement('c-sfc-action', {
            is: SfcAction
        });
        document.body.appendChild(element);

        return Promise.resolve().then(() => {
            const button = element.shadowRoot.querySelector('button');

            // Simulate keyboard interaction
            const keydownEvent = new KeyboardEvent('keydown', { key: 'Enter' });
            button.dispatchEvent(keydownEvent);

            // Verify action was triggered
            expect(button).toBeDefined();
        });
    });
});

Snapshot Testing

Usage: Use snapshot testing for UI regression testing (optional).

Example:

import { createElement } from 'lwc';
import SfcAction from 'c/sfcAction';

describe('c-sfc-action', () => {
    it('matches snapshot', () => {
        const element = createElement('c-sfc-action', {
            is: SfcAction
        });
        element.label = 'Click Me';
        document.body.appendChild(element);

        return Promise.resolve().then(() => {
            expect(element).toMatchSnapshot();
        });
    });
});

LWC Mocking Strategies

Usage: Mock wire adapters, Apex methods, and modules in LWC tests.

Example:

// Mock wire adapter
jest.mock(
    'lightning/uiRecordApi',
    () => ({
        getRecord: jest.fn()
    }),
    { virtual: true }
);

// Mock Apex method
jest.mock(
    '@salesforce/apex/SFC_AccountService.getAccounts',
    () => ({ default: jest.fn() }),
    { virtual: true }
);

// Mock module
jest.mock('c/utils', () => ({
    formatDate: jest.fn((date) => date.toISOString())
}));

When to Mock vs. Use Real Data (LWC)

Mock when:

  • Testing error scenarios
  • Testing with specific data that's hard to create
  • Testing external integrations
  • Improving test performance

Use real data when:

  • Testing data transformation logic
  • Testing component behavior with actual data
  • Testing complex business logic
  • Integration testing

Mock Data Patterns (LWC)

Create reusable mock data generators.

Example:

// Mock data generator
const createMockAccount = (overrides = {}) => {
    return {
        Id: '001000000000000AAA',
        Name: 'Test Account',
        Type: 'Customer',
        ...overrides
    };
};

// Usage in tests
it('displays account name', () => {
    const mockAccount = createMockAccount({ Name: 'Custom Account' });
    getRecord.mockResolvedValue({ fields: { Name: { value: mockAccount.Name } } });
    // Test implementation
});

Change History

Version 1.0 - 2026-01-14

  • Initial release of testing standards documentation

Last Updated: 2026-01-14
Version: 1.0