Apex Testing Standards
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
Testsuffix - 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>
- Primary test method covers the majority/happy path:
- 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
@IsTestannotation - 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:
- Document why it's necessary
- Explain why test data creation isn't sufficient
- 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 andtest_<methodToTest>_<qualifier>for additional paths - Use
@IsTestannotation - 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 Management → Test 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()andTest.stopTest()around the behavior under test - Prefer transaction rollback isolation over manual cleanup when possible
For broader isolation guidance, see Test Data Management → Test 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:
- Required: Test successful execution paths (happy path)
- Required: Test all conditional logic branches (if/else true and false, all switch cases)
- Required: Test edge cases that affect business logic (empty lists, boundary values)
- Required: Test security violations (CRUD/FLS) when security is a concern
- 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 Practices → Testing 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
@TestSetupor 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):
- Document why it's necessary
- Explain why test data creation isn't sufficient
- 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 checksAssert.areNotEqual(unexpected, actual, message)- For inequality checksAssert.isTrue(condition, message)- For true conditionsAssert.isFalse(condition, message)- For false conditionsAssert.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:
- Required: Test successful execution paths (happy path)
- Required: Test all conditional branches (if/else true and false, all switch cases)
- 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:
- Class-level documentation
@TestSetupmethod (if needed)- Helper methods (if needed)
- 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
Test-Related Code Analyzer Rules
Key rules for test classes:
ApexAssertionsShouldIncludeMessage- All assertions must include messagesApexUnitTestClassShouldHaveAsserts- Test classes must have at least one assertionApexUnitTestMethodShouldHaveIsTestAnnotation- Test methods must use@IsTestApexUnitTestShouldNotUseSeeAllDataTrue- AvoidseeAllData=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