Test Classes Deep Dive – 100% Coverage the Right Way & Test Data Setup

Test classes are mandatory in Salesforce. You need at least 75% code coverage to deploy to production, but aiming for 100% with meaningful tests is the real goal. This guide covers writing effective test classes and test data setup.

1. The Right Way to Write Test Classes

Basic Test Class Structure


@isTest
private class AccountServiceTest {
    
    @TestSetup
    static void setup() {
        // Test data creation runs once for all tests
        insertTestAccounts();
    }
    
    @isTest
    static void testPositiveScenario() {
        // Arrange
        List<Account> accounts = [SELECT Id FROM Account LIMIT 1];
        
        // Act
        AccountService.processAccounts(accounts);
        
        // Assert
        List<Account> updatedAccounts = [SELECT Id, Rating FROM Account];
        System.assertEquals('Hot', updatedAccounts[0].Rating);
    }
    
    @isTest
    static void testNegativeScenario() {
        Account acc = new Account(Name = 'Test Account', Revenue = 0);
        insert acc;
        
        try {
            AccountService.validateRevenue(acc);
            System.assert(false, 'Should have thrown exception');
        } catch (CustomException e) {
            System.assert(e.getMessage().contains('Revenue required'));
        }
    }
    
    @isTest
    static void testEdgeCase() {
        // Test boundary conditions, null values, empty lists
        AccountService.processAccounts(new List<Account>());
        // Should not throw error
    }
    
    private static void insertTestAccounts() {
        List<Account> accounts = new List<Account>();
        for (Integer i = 0; i < 10; i++) {
            accounts.add(new Account(
                Name = 'Test Account ' + i,
                Revenue = 100000 + (i * 10000)
            ));
        }
        insert accounts;
    }
}
  

2. Test Data Factory Pattern

Use a dedicated factory class to create test data. This keeps your tests clean and DRY.


@isTest
public class TestDataFactory {
    
    public static Account createAccount(String name, String rating) {
        Account acc = new Account(
            Name = name,
            Rating = rating,
            Phone = '555-1234'
        );
        insert acc;
        return acc;
    }
    
    public static List<Contact> createContacts(Integer count, Id accountId) {
        List<Contact> contacts = new List<Contact>();
        for (Integer i = 0; i < count; i++) {
            contacts.add(new Contact(
                FirstName = 'Test',
                LastName = 'Contact ' + i,
                AccountId = accountId,
                Email = 'test' + i + '@example.com'
            ));
        }
        insert contacts;
        return contacts;
    }
    
    public static Opportunity createOpportunity(Id accountId, String stageName) {
        Opportunity opp = new Opportunity(
            Name = 'Test Opportunity',
            AccountId = accountId,
            StageName = stageName,
            CloseDate = Date.today().addDays(30),
            Amount = 50000
        );
        insert opp;
        return opp;
    }
}

// Usage in tests
@isTest
private class OpportunityServiceTest {
    @isTest
    static void testOpportunityStage() {
        Account acc = TestDataFactory.createAccount('Acme Corp', 'Hot');
        Opportunity opp = TestDataFactory.createOpportunity(acc.Id, 'Prospecting');
        
        OpportunityService.updateStage(opp.Id, 'Negotiation/Review');
        
        Opportunity updatedOpp = [SELECT StageName FROM Opportunity WHERE Id = :opp.Id];
        System.assertEquals('Negotiation/Review', updatedOpp.StageName);
    }
}
  

3. Testing Bulk Data

Tests should verify your code works with bulk data. This catches bulkification issues and governor limit problems.


@isTest
private class BulkAccountTriggerTest {
    
    @isTest
    static void testBulkInsert() {
        List<Account> accounts = new List<Account>();
        
        // Create 200 accounts (max bulk size)
        for (Integer i = 0; i < 200; i++) {
            accounts.add(new Account(
                Name = 'Bulk Account ' + i,
                BillingCity = 'San Francisco'
            ));
        }
        
        insert accounts;
        
        // Verify trigger processed all records
        List<Account> verifyAccounts = [SELECT Id, Description FROM Account WHERE Name LIKE 'Bulk Account%'];
        System.assertEquals(200, verifyAccounts.size());
        
        for (Account acc : verifyAccounts) {
            System.assert(acc.Description != null);
        }
    }
}
  

4. Testing Triggers The Right Way


@isTest
private class AccountTriggerTest {
    
    @TestSetup
    static void setup() {
        TestDataFactory.createAccount('Test Account', 'Hot');
    }
    
    @isTest
    static void testBeforeInsert() {
        Account acc = new Account(
            Name = 'New Account',
            BillingCity = null
        );
        
        insert acc;
        
        // Trigger should set default city
        Account inserted = [SELECT BillingCity FROM Account WHERE Id = :acc.Id];
        System.assertEquals('San Francisco', inserted.BillingCity);
    }
    
    @isTest
    static void testAfterInsert() {
        Account parentAcc = [SELECT Id FROM Account LIMIT 1];
        
        Account newAcc = new Account(
            Name = 'Child Account',
            ParentId = parentAcc.Id,
            Type = 'Subsidiary'
        );
        insert newAcc;
        
        // Trigger should create activity
        List<Task> tasks = [SELECT Id FROM Task WHERE WhoId = :newAcc.Id];
        System.assertEquals(1, tasks.size());
    }
    
    @isTest
    static void testTriggerNotRecursive() {
        Account acc = [SELECT Id FROM Account LIMIT 1];
        acc.Name = 'Updated Name';
        
        update acc;  // Should not cause infinite loop
        
        // Add assertion to verify expected behavior
        System.assert(true);  // Passed without recursion error
    }
}
  

5. Achieving 100% Code Coverage

Coverage Checklist


✓ Happy path (normal execution)
✓ Negative scenarios (exceptions, invalid data)
✓ Edge cases (null values, empty lists, boundary conditions)
✓ All if/else branches
✓ All loops with different iteration counts
✓ Exception handling (try/catch blocks)
✓ Bulk operations (200 records)
  

Finding Uncovered Lines

In Developer Console:

6. Best Practices

Use @isTest Annotation


// Mark test classes and methods
@isTest
private class MyTest {
    @isTest
    static void testSomething() { }
}

// Mark test utilities
@isTest
public class TestHelper { }
  

Keep Test Data Isolated


// Use System.Test.startTest() and stopTest()
@isTest
static void testWithIsolation() {
    Account acc = TestDataFactory.createAccount('Test', 'Hot');
    
    Test.startTest();
    // Test code here - governor limits reset
    AccountService.processAccount(acc);
    Test.stopTest();
    
    // Assertions after stopTest()
}
  

Key Takeaways