Writing test classes for Apex triggers is mandatory for deployment to production. Every trigger requires at least 75% code coverage, and test classes must validate both positive and negative scenarios. This guide covers trigger test patterns, schedulable class testing, batch class coverage, and proper use of System.assertEquals assertions.
Basic Test Class Structure for Triggers
A trigger test class follows the standard Apex test pattern with @isTest annotation and testMethod declarations. The test must create test data, execute the trigger logic, and assert expected outcomes using System.assertEquals or similar assertion methods.
Apex Trigger Code:
trigger accountAfterInsert on Account (after insert) {
string managerId= [Select Id, ManagerId FROM User WHERE Id = :userInfo.getUserId()].ManagerId;
for(Account acc: trigger.New){
AccountShare accShare = new AccountShare();
accShare .ParentId = acc.Id;
accShare .UserOrGroupId = managerId;
accShare .AccessLevel = 'EDIT';
accShare .RowCause = Schema.accountShare.RowCause.Manual;
}
}
// Unit Test for above Trigger
@isTest
private class AccountTriggersTest{
private static testmethod void accountTriggersTest(){
Account acc = new Account();
acc.name = 'NewAccount';
acc.address = 'USA';
insert acc;
}
}
Enhanced Test Class with System.assertEquals Assertions
The basic example above lacks proper assertions and error handling. A production-ready test class must verify the trigger’s behavior using System.assertEquals and handle edge cases.
@isTest
private class AccountTriggersTest {
@testSetup
static void setupTestData() {
// Create test users with manager hierarchy
Profile standardProfile = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1];
User manager = new User(
FirstName = 'Test',
LastName = 'Manager',
Email = 'testmanager@example.com',
Username = 'testmanager@example.com.test',
Alias = 'tmgr',
ProfileId = standardProfile.Id,
TimeZoneSidKey = 'America/New_York',
LocaleSidKey = 'en_US',
EmailEncodingKey = 'UTF-8',
LanguageLocaleKey = 'en_US'
);
insert manager;
User testUser = new User(
FirstName = 'Test',
LastName = 'User',
Email = 'testuser@example.com',
Username = 'testuser@example.com.test',
Alias = 'tuser',
ProfileId = standardProfile.Id,
ManagerId = manager.Id,
TimeZoneSidKey = 'America/New_York',
LocaleSidKey = 'en_US',
EmailEncodingKey = 'UTF-8',
LanguageLocaleKey = 'en_US'
);
insert testUser;
}
@isTest
static void testAccountTriggerPositive() {
// Get test user with manager
User testUser = [SELECT Id, ManagerId FROM User WHERE Username = 'testuser@example.com.test' LIMIT 1];
System.runAs(testUser) {
Test.startTest();
// Create account to trigger the sharing logic
Account testAccount = new Account(
Name = 'Test Account',
BillingStreet = '123 Main St',
BillingCity = 'New York',
BillingState = 'NY',
BillingCountry = 'USA'
);
insert testAccount;
Test.stopTest();
// Verify AccountShare record was created
List shares = [SELECT Id, ParentId, UserOrGroupId, AccessLevel
FROM AccountShare
WHERE ParentId = :testAccount.Id
AND RowCause = 'Manual'];
System.assertEquals(1, shares.size(), 'Expected one AccountShare record');
System.assertEquals(testUser.ManagerId, shares[0].UserOrGroupId, 'Manager should have access');
System.assertEquals('Edit', shares[0].AccessLevel, 'Access level should be Edit');
}
}
@isTest
static void testAccountTriggerNoManager() {
// Test scenario where user has no manager
User userNoManager = [SELECT Id FROM User WHERE Username = 'testmanager@example.com.test' LIMIT 1];
System.runAs(userNoManager) {
Test.startTest();
try {
Account testAccount = new Account(
Name = 'Test Account No Manager',
BillingCountry = 'USA'
);
insert testAccount;
// Verify no sharing records created when manager is null
List shares = [SELECT Id FROM AccountShare
WHERE ParentId = :testAccount.Id
AND RowCause = 'Manual'];
System.assertEquals(0, shares.size(), 'No shares should be created without manager');
} catch (Exception e) {
// Handle potential null pointer exceptions gracefully
System.assert(e.getMessage().contains('null'), 'Expected null manager error');
}
Test.stopTest();
}
}
}
Test Class for Schedulable Apex Classes
Schedulable classes require different test patterns since they execute asynchronously. Use Test.startTest() and Test.stopTest() to force synchronous execution in tests.
// Example Schedulable Class
public class AccountCleanupSchedulable implements Schedulable {
public void execute(SchedulableContext sc) {
AccountCleanupBatch batch = new AccountCleanupBatch();
Database.executeBatch(batch, 200);
}
}
// Test Class for Schedulable Class
@isTest
private class AccountCleanupSchedulableTest {
@isTest
static void testSchedulableExecution() {
Test.startTest();
// Schedule the job
String cronExp = '0 0 0 1 1 ? 2025';
String jobId = System.schedule('Test Account Cleanup', cronExp, new AccountCleanupSchedulable());
// Verify job was scheduled
CronTrigger ct = [SELECT Id, CronExpression, TimesTriggered, NextFireTime
FROM CronTrigger WHERE Id = :jobId];
System.assertEquals(cronExp, ct.CronExpression, 'Cron expression should match');
System.assertEquals(0, ct.TimesTriggered, 'Job should not have run yet');
Test.stopTest();
// After Test.stopTest(), scheduled job executes synchronously
// Verify batch job was queued (implementation depends on batch logic)
}
}
Test Class for Batch Apex Classes
Batch classes require testing the start, execute, and finish methods separately. Create test data within governor limits and verify batch processing logic.
// Example Batch Class public class AccountCleanupBatch implements Database.Batchable{ public Database.QueryLocator start(Database.BatchableContext bc) { return Database.getQueryLocator( 'SELECT Id, Name FROM Account WHERE LastModifiedDate < LAST_N_DAYS:365' ); } public void execute(Database.BatchableContext bc, List accounts) { delete accounts; } public void finish(Database.BatchableContext bc) { // Send completion email or other cleanup System.debug('Batch completed: ' + bc.getJobId()); } } // Test Class for Batch Class @isTest private class AccountCleanupBatchTest { @testSetup static void setupTestData() { List oldAccounts = new List (); for (Integer i = 0; i < 200; i++) { oldAccounts.add(new Account( Name = 'Old Account ' + i, BillingCountry = 'USA' )); } insert oldAccounts; // Update LastModifiedDate to simulate old records Test.setCreatedDate(oldAccounts[0].Id, Date.today().addDays(-400)); } @isTest static void testBatchExecution() { Test.startTest(); AccountCleanupBatch batch = new AccountCleanupBatch(); Id jobId = Database.executeBatch(batch, 100); Test.stopTest(); // Verify batch job completed AsyncApexJob job = [SELECT Id, Status, NumberOfErrors, JobItemsProcessed FROM AsyncApexJob WHERE Id = :jobId]; System.assertEquals('Completed', job.Status, 'Batch should complete successfully'); System.assertEquals(0, job.NumberOfErrors, 'No errors expected'); // Verify records were processed (actual deletion depends on test data age) List remainingAccounts = [SELECT Id FROM Account]; System.assert(remainingAccounts.size() >= 0, 'Batch should process accounts'); } }
System.assertEquals Best Practices in Test Classes
System.assertEquals validates expected vs actual values in test methods. Always include meaningful assertion messages for debugging failed tests.
Common System.assertEquals Patterns
- Basic assertion:
System.assertEquals(expected, actual, 'Error message') - List size:
System.assertEquals(5, accounts.size(), 'Should create 5 accounts') - Field values:
System.assertEquals('Active', account.Status__c, 'Status should be Active') - Boolean checks:
System.assertEquals(true, account.IsActive__c, 'Account should be active') - Null checks:
System.assertEquals(null, account.ManagerId, 'Manager should be null')
Alternative Assertion Methods
System.assertNotEquals()– Verify values are differentSystem.assert()– Boolean condition testingSystem.assertNotEquals(null, value)– Verify non-null values
Governor Limits and Test Class Considerations
Test classes must respect Salesforce governor limits even in test context. Key limits for trigger testing:
- SOQL queries: 100 per transaction (synchronous)
- DML statements: 150 per transaction
- Records per DML: 10,000 maximum
- CPU time: 10,000ms synchronous, 60,000ms asynchronous
- Heap size: 6MB synchronous, 12MB asynchronous
Use @testSetup methods to create test data once and share across test methods. This reduces DML statements and improves test performance.
Common Test Class Errors and Solutions
MIXED_DML_OPERATION Error
Occurs when mixing setup and non-setup object DML in the same transaction. Solution: Use System.runAs() or separate transactions.
// Incorrect - causes MIXED_DML_OPERATION
User testUser = new User(/* user fields */);
insert testUser;
Account testAccount = new Account(Name = 'Test');
insert testAccount; // Error here
// Correct - separate transactions
System.runAs(new User(Id = UserInfo.getUserId())) {
User testUser = new User(/* user fields */);
insert testUser;
}
Account testAccount = new Account(Name = 'Test');
insert testAccount;
Insufficient Code Coverage
Ensure test methods exercise all trigger scenarios: insert, update, delete, undelete. Cover both positive and negative test cases.
Frequently Asked Questions
How do I write test class for schedulable class in Salesforce?
Create a test class with @isTest annotation, use System.schedule() to schedule the job within Test.startTest() and Test.stopTest() blocks, then verify the job was scheduled by querying CronTrigger records. The schedulable logic executes synchronously during test execution.
What is System.assertEquals used for in Apex test classes?
System.assertEquals compares expected and actual values in test methods. It takes three parameters: expected value, actual value, and an optional error message. Use it to verify that your code produces the correct results, such as field values, record counts, or object states.
How to write test class for batch class in Salesforce?
Create test data in @testSetup, then use Database.executeBatch() within Test.startTest() and Test.stopTest() blocks. After Test.stopTest(), query AsyncApexJob to verify batch completion status and check that your batch logic processed records correctly.
What are the requirements for trigger test class code coverage?
Trigger test classes must achieve at least 75% code coverage for deployment to production. Best practice is 100% coverage by testing all trigger contexts (insert, update, delete, undelete) and both positive and negative scenarios with proper assertions.
How do I test scheduled Apex classes that run batch jobs?
Test the schedulable class by verifying it schedules correctly, then test the batch class separately. In the schedulable test, use System.schedule() and verify the CronTrigger record. For the batch portion, create a separate test method that calls Database.executeBatch() directly.