Apex Unit Test Class: Complete Guide with Examples | SalesforceTutorial

Written by Prasanth Kumar Published on Updated on

Apex unit test classes verify that your code works correctly before deployment to production. Every Salesforce org requires at least 75% code coverage to deploy Apex code, making test classes essential for any Salesforce development project.

What is an Apex Unit Test Class?

A unit test class is an Apex class annotated with @isTest that contains test methods to validate your code’s functionality. Test methods simulate user interactions and data scenarios to ensure your Apex code handles various conditions correctly.

Unit tests run in isolation from your org’s data and create temporary test data that gets deleted after execution. This isolation ensures tests are reliable and don’t interfere with production data.

Essential Rules for Writing Test Classes

Before writing any test class, understand these critical requirements:

  • 75% code coverage minimum – Required for production deployment
  • Test data isolation – Test data doesn’t save to the database permanently
  • No SOQL queries on production data – Always create test data within your test methods
  • Avoid seeAllData=true – Create your own test data instead
  • Use assertions extensively – System.assertEquals and System.assertNotEquals validate expected outcomes
  • Governor limit resets – Use System.startTest() and System.stopTest() to reset limits for specific operations

Basic Test Class Structure and Syntax

Here’s the fundamental structure of an Apex test class:

@isTest
private class MyTestClass {
    @testSetup
    static void setupTestData() {
        // Create test data used by multiple test methods
    }
    
    @isTest
    static void testMethodName() {
        // Test logic here
        System.startTest();
        // Code being tested
        System.stopTest();
        
        // Assertions
        System.assertEquals(expectedValue, actualValue, 'Error message');
    }
}

System.assertEquals in Apex Test Class

System.assertEquals is the primary assertion method for validating test results. It compares expected values with actual results and fails the test if they don’t match.

// Basic syntax
System.assertEquals(expected, actual, 'Optional error message');

// Examples
System.assertEquals(5, accountList.size(), 'Should create 5 accounts');
System.assertEquals('Test Account', acc.Name, 'Account name should match');
System.assertEquals(true, acc.IsActive__c, 'Account should be active');

Other useful assertion methods:

  • System.assertNotEquals() – Verifies values are different
  • System.assert() – Verifies a condition is true

Test Class for Regular Apex Classes

Here’s an improved version of a basic test class with proper structure and assertions:

Apex Class:

public class AccountCreationController {
    public Account account {get; set;}
    
    public AccountCreationController() {
        account = new Account();
    }
    
    public PageReference save() {
        try {
            if (account.Name != null && account.Name.trim() != '') {
                insert account;
                return new PageReference('/001/o'); // Account list view
            }
        } catch (DmlException e) {
            ApexPages.addMessage(new ApexPages.Message(
                ApexPages.Severity.ERROR, 'Error creating account: ' + e.getMessage()));
        }
        return null;
    }
}

Test Class:

@isTest
private class AccountCreationControllerTest {
    
    @isTest
    static void testSuccessfulAccountCreation() {
        // Setup test data
        AccountCreationController controller = new AccountCreationController();
        controller.account.Name = 'Test Account';
        controller.account.Type = 'Customer';
        
        System.startTest();
        PageReference result = controller.save();
        System.stopTest();
        
        // Verify account was created
        List createdAccounts = [SELECT Id, Name FROM Account WHERE Name = 'Test Account'];
        System.assertEquals(1, createdAccounts.size(), 'One account should be created');
        System.assertEquals('Test Account', createdAccounts[0].Name, 'Account name should match');
        System.assertNotEquals(null, result, 'Should return redirect page reference');
    }
    
    @isTest
    static void testAccountCreationWithEmptyName() {
        AccountCreationController controller = new AccountCreationController();
        controller.account.Name = '';
        
        System.startTest();
        PageReference result = controller.save();
        System.stopTest();
        
        // Verify no account was created
        List createdAccounts = [SELECT Id FROM Account];
        System.assertEquals(0, createdAccounts.size(), 'No account should be created with empty name');
        System.assertEquals(null, result, 'Should return null for invalid input');
    }
}

Test Class for Batch Class

Batch classes require specific testing patterns to validate start, execute, and finish methods:

@isTest
private class AccountBatchProcessorTest {
    
    @testSetup
    static void setupTestData() {
        List accounts = new List();
        for (Integer i = 0; i < 200; i++) {
            accounts.add(new Account(
                Name = 'Test Account ' + i,
                Type = 'Prospect'
            ));
        }
        insert accounts;
    }
    
    @isTest
    static void testBatchExecution() {
        System.startTest();
        AccountBatchProcessor batch = new AccountBatchProcessor();
        Id jobId = Database.executeBatch(batch, 100);
        System.stopTest();
        
        // Verify batch processed all records
        List processedAccounts = [SELECT Id, Type FROM Account WHERE Type = 'Customer'];
        System.assertEquals(200, processedAccounts.size(), 'All accounts should be processed');
        
        // Verify async job completed
        AsyncApexJob job = [SELECT Status, NumberOfErrors FROM AsyncApexJob WHERE Id = :jobId];
        System.assertEquals('Completed', job.Status, 'Batch job should complete successfully');
        System.assertEquals(0, job.NumberOfErrors, 'Batch should have no errors');
    }
}

Test Class for Schedulable Class

Schedulable classes need tests that verify the scheduled execution and the actual job logic:

@isTest
private class AccountCleanupSchedulerTest {
    
    @testSetup
    static void setupTestData() {
        List accounts = new List();
        for (Integer i = 0; i < 50; i++) {
            accounts.add(new Account(
                Name = 'Old Account ' + i,
                CreatedDate = Date.today().addDays(-365)
            ));
        }
        insert accounts;
    }
    
    @isTest
    static void testScheduledExecution() {
        String cronExpression = '0 0 2 * * ?'; // Daily at 2 AM
        
        System.startTest();
        String jobId = System.schedule('Account Cleanup Test', cronExpression, new AccountCleanupScheduler());
        System.stopTest();
        
        // Verify job was scheduled
        CronTrigger ct = [SELECT Id, CronExpression, State FROM CronTrigger WHERE Id = :jobId];
        System.assertEquals(cronExpression, ct.CronExpression, 'Cron expression should match');
        System.assertEquals('WAITING', ct.State, 'Job should be in waiting state');
    }
    
    @isTest
    static void testSchedulableExecuteMethod() {
        AccountCleanupScheduler scheduler = new AccountCleanupScheduler();
        
        System.startTest();
        scheduler.execute(null); // SchedulableContext can be null in tests
        System.stopTest();
        
        // Verify the scheduled logic executed correctly
        List remainingAccounts = [SELECT Id FROM Account];
        System.assertEquals(0, remainingAccounts.size(), 'Old accounts should be deleted');
    }
}

Test Class for Schedulable Batch Class

When testing a schedulable class that executes a batch job, test both the scheduling and batch execution:

@isTest
private class ScheduledBatchProcessorTest {
    
    @testSetup
    static void setupTestData() {
        List contacts = new List();
        for (Integer i = 0; i < 100; i++) {
            contacts.add(new Contact(
                FirstName = 'Test',
                LastName = 'Contact ' + i,
                Email = 'test' + i + '@example.com'
            ));
        }
        insert contacts;
    }
    
    @isTest
    static void testScheduledBatchExecution() {
        ScheduledBatchProcessor scheduler = new ScheduledBatchProcessor();
        String cronExpression = '0 0 1 * * ?';
        
        System.startTest();
        String jobId = System.schedule('Batch Processor Test', cronExpression, scheduler);
        
        // Manually execute the schedulable to test batch execution
        scheduler.execute(null);
        System.stopTest();
        
        // Verify batch job was queued
        List batchJobs = [SELECT Status FROM AsyncApexJob WHERE JobType = 'BatchApex'];
        System.assert(batchJobs.size() > 0, 'Batch job should be queued');
        
        // Verify contacts were processed (depends on your batch logic)
        List processedContacts = [SELECT Id, Email FROM Contact WHERE Email LIKE '%@processed.com'];
        System.assertEquals(100, processedContacts.size(), 'All contacts should be processed');
    }
}

Best Practices for Test Classes

Use @testSetup for Shared Test Data

When multiple test methods need the same data, use @testSetup to create it once:

@testSetup
static void setupTestData() {
    Profile standardProfile = [SELECT Id FROM Profile WHERE Name = 'Standard User'];
    User testUser = new User(
        FirstName = 'Test',
        LastName = 'User',
        Email = 'testuser@example.com',
        Username = 'testuser@example.com.test',
        Alias = 'tuser',
        ProfileId = standardProfile.Id,
        TimeZoneSidKey = 'America/New_York',
        LocaleSidKey = 'en_US',
        EmailEncodingKey = 'UTF-8',
        LanguageLocaleKey = 'en_US'
    );
    insert testUser;
}

Test Both Positive and Negative Scenarios

Always test success cases and failure conditions:

@isTest
static void testValidInput() {
    // Test with valid data
}

@isTest
static void testInvalidInput() {
    // Test with invalid data, expect exceptions
    try {
        // Code that should fail
        System.assert(false, 'Expected exception was not thrown');
    } catch (CustomException e) {
        System.assertEquals('Expected error message', e.getMessage());
    }
}

Governor Limit Testing

Use System.startTest() and System.stopTest() to reset governor limits for the code being tested:

@isTest
static void testBulkProcessing() {
    List accounts = new List();
    for (Integer i = 0; i < 200; i++) {
        accounts.add(new Account(Name = 'Bulk Account ' + i));
    }
    
    System.startTest(); // Resets governor limits
    insert accounts;
    System.stopTest(); // Limits are enforced again
    
    System.assertEquals(200, [SELECT COUNT() FROM Account], 'All accounts should be inserted');
}

Common Test Class Errors and Solutions

Insufficient Code Coverage

If your test coverage is below 75%, ensure you’re testing all code paths:

  • Test all if/else branches
  • Test exception handling with try/catch blocks
  • Test all public methods and properties
  • Use System.runAs() to test different user contexts

SOQL Queries in Test Methods

Avoid querying production data. Create test data instead:

// Bad - queries production data
List accounts = [SELECT Id FROM Account LIMIT 10];

// Good - creates test data
List accounts = new List();
for (Integer i = 0; i < 10; i++) {
    accounts.add(new Account(Name = 'Test Account ' + i));
}
insert accounts;

Testing Scheduled Apex in Salesforce

When writing test classes for scheduled Apex, focus on three key areas:

  1. Scheduling verification – Confirm the job schedules correctly
  2. Execute method testing – Test the actual business logic
  3. Error handling – Verify graceful failure handling

For complex scheduled operations, consider separating the scheduling logic from the business logic to make testing easier.

Frequently Asked Questions

How do I write a test class for a scheduled Apex class?

Create a test class with @isTest annotation and test both the scheduling and execution. Use System.schedule() to verify scheduling works, then call the execute() method directly to test the business logic. Always use System.startTest() and System.stopTest() around the execution.

What is System.assertEquals used for in test classes?

System.assertEquals compares expected values with actual results in your test methods. It takes three parameters: expected value, actual value, and an optional error message. If the values don’t match, the test fails with the provided message.

How do I test a schedulable batch class in Salesforce?

Test both the schedulable interface and the batch execution separately. Schedule the job using System.schedule(), then manually call the execute() method to trigger the batch job. Verify the batch job was queued by checking AsyncApexJob records and validate the batch processing results.

Why can’t I use SOQL queries on production data in test classes?

Test classes run in isolation to ensure reliable, repeatable results. Querying production data makes tests dependent on existing data, which can change and cause tests to fail unexpectedly. Always create your own test data within test methods using DML operations.

What happens to test data after a test class runs?

All data created during test execution is automatically deleted when the test completes. This includes records inserted via DML operations. Test data never persists to your org’s database, ensuring tests don’t affect production data.

How do I achieve 75% code coverage for deployment?

Write test methods that execute all lines of your Apex code. Test all conditional branches (if/else), exception handling blocks, and public methods. Use code coverage reports in Developer Console or VS Code to identify untested lines, then write additional test methods to cover those scenarios.