Testing Apex – Apex Unit Test

Testing Apex means writing unit tests that verify how Apex classes, triggers, controllers, and asynchronous jobs behave before the code is deployed. Salesforce provides an Apex testing framework to write tests, run tests, view test results, and review code coverage.

An Apex unit test is a test method written in Apex. It creates or prepares test data, runs a small piece of Apex logic, and then uses assertions such as System.assertEquals() to confirm the result. Salesforce keeps test data separate from normal organization data, and changes made by tests are rolled back after the test run.

Official reference: Salesforce documents Apex test classes, test execution, and code coverage in the Apex unit testing guide and the Apex test running guide.

Why Apex Unit Tests are Required Before Deployment

Apex unit tests are not only a development practice; they are also part of Salesforce deployment requirements. Before deploying Apex to production or packaging code for AppExchange, these conditions must be satisfied.

  • At least 75% of the total Apex code in the org must be covered by unit tests.
  • All selected Apex tests must pass for the deployment to succeed.
  • Every Apex trigger must have some test coverage.
  • All Apex classes and triggers must compile successfully.

Do not treat 75% as the quality target. A test suite should cover important business paths, error paths, bulk records, and security-sensitive behavior. High coverage without meaningful assertions can still leave defects in production.

What to Test in Apex Classes and Triggers

Testing through the Salesforce user interface can show whether a screen works, but it does not prove that Apex behaves correctly in every data condition. Apex code can also run from APIs, imports, flows, integrations, scheduled jobs, and bulk operations. Unit tests should cover the behavior of the Apex code directly.

  • Single-record behavior: verify that one record produces the expected result.
  • Bulk behavior: test lists of records, especially trigger logic that may receive up to 200 records in one transaction.
  • Positive behavior: confirm that valid input produces the expected update, insert, return value, or exception-free result.
  • Negative behavior: confirm how the code behaves when input is missing, invalid, duplicated, or outside business rules.
  • Restricted-user behavior: use System.runAs() when the logic depends on user context, sharing, ownership, or permissions.
  • Asynchronous behavior: test future methods, Queueable Apex, Batch Apex, and scheduled Apex with Test.startTest() and Test.stopTest().

The 3 A’s Pattern for Apex Unit Tests

A practical Apex test method usually follows the 3 A’s of unit testing: Arrange, Act, and Assert. This keeps the test readable and helps future developers understand exactly what behavior is being checked.

StepMeaning in an Apex test
ArrangeCreate test records, set field values, and prepare the required state.
ActRun the Apex method, DML operation, trigger, queueable job, or batch job being tested.
AssertUse assertions to compare the actual result with the expected result.

What are Apex Unit test?

Apex supports creating and executing Apex unit test methods for robust and error-free code. An Apex unit test is a static method that verifies the working behavior of Apex code. Test methods do not take arguments and cannot be defined inside triggers.

When we want to create a test method in Apex, we can use the older testmethod keyword or the modern @isTest annotation. In current Apex code, the @isTest annotation is preferred because it is clearer and can be used on the test class and test methods.

Legacy Apex testmethod Keyword Example

The original legacy syntax is kept below. It shows the older testmethod style.

</>
Copy
public class myclass {
	Static testmethod void myTest() {
		//test method logic using System.assert(),System.assertEquals()
		//and System.assertNotEquals()
	}
}

@isTest Annotation for Apex Test Classes

If a method is defined with @isTest, Salesforce treats it as a test method. A test class is commonly declared as private, and each test method should be static.

@isTest
private class myClass {
     static testMethod void myTest() {
        // code_block
    }
}

A more current version of the same test class style is shown below.

</>
Copy
@IsTest
private class MyClassTest {
    @IsTest
    static void myTest() {
        // Arrange
        Account accountRecord = new Account(Name = 'Test Account');

        // Act
        insert accountRecord;

        // Assert
        Account savedAccount = [
            SELECT Id, Name
            FROM Account
            WHERE Id = :accountRecord.Id
        ];
        System.assertEquals('Test Account', savedAccount.Name);
    }
}

Apex Unit Test Example with Assertions

The next example tests a small service class. The service marks high-value accounts as Hot based on annual revenue. The test method checks the expected result with System.assertEquals().

</>
Copy
public class AccountRatingService {
    public static void markHighValue(List<Account> accounts) {
        for (Account accountRecord : accounts) {
            if (accountRecord.AnnualRevenue != null &&
                accountRecord.AnnualRevenue >= 1000000) {
                accountRecord.Rating = 'Hot';
            }
        }
    }
}
</>
Copy
@IsTest
private class AccountRatingServiceTest {
    @IsTest
    static void highRevenueAccountIsMarkedHot() {
        // Arrange
        Account accountRecord = new Account(
            Name = 'Acme Test',
            AnnualRevenue = 1000000
        );

        // Act
        AccountRatingService.markHighValue(
            new List<Account> { accountRecord }
        );

        // Assert
        System.assertEquals('Hot', accountRecord.Rating);
    }

    @IsTest
    static void lowRevenueAccountKeepsRatingBlank() {
        // Arrange
        Account accountRecord = new Account(
            Name = 'Small Test',
            AnnualRevenue = 1000
        );

        // Act
        AccountRatingService.markHighValue(
            new List<Account> { accountRecord }
        );

        // Assert
        System.assertEquals(null, accountRecord.Rating);
    }
}

This test has one positive case and one negative case. It is better than checking only code coverage because it verifies the expected behavior.

Best Approach to Test an Apex Trigger

The best approach is to keep trigger logic thin and move business logic into a handler or service class. Then write tests that perform DML and verify the result after the trigger runs. This checks the real trigger path instead of testing only the helper method.

</>
Copy
trigger AccountTrigger on Account (before insert, before update) {
    AccountRatingService.markHighValue(Trigger.new);
}
</>
Copy
@IsTest
private class AccountTriggerTest {
    @IsTest
    static void beforeInsertMarksHighValueAccountHot() {
        // Arrange
        Account accountRecord = new Account(
            Name = 'Trigger Test Account',
            AnnualRevenue = 1000000
        );

        // Act
        Test.startTest();
        insert accountRecord;
        Test.stopTest();

        // Assert
        Account savedAccount = [
            SELECT Rating
            FROM Account
            WHERE Id = :accountRecord.Id
        ];
        System.assertEquals('Hot', savedAccount.Rating);
    }
}

Bulk Apex Unit Test for 200 Trigger Records

Triggers can receive many records in one transaction, so a test class should include a bulk case. A common bulk test creates 200 records, performs one DML statement, and then verifies that all records were processed correctly.

</>
Copy
@IsTest
private class AccountTriggerBulkTest {
    @IsTest
    static void beforeInsertHandlesTwoHundredAccounts() {
        // Arrange
        List<Account> accounts = new List<Account>();
        for (Integer i = 0; i < 200; i++) {
            accounts.add(new Account(
                Name = 'Bulk Account ' + i,
                AnnualRevenue = 1000000
            ));
        }

        // Act
        Test.startTest();
        insert accounts;
        Test.stopTest();

        // Assert
        Set<Id> accountIds = new Map<Id, Account>(accounts).keySet();
        Integer hotCount = [
            SELECT COUNT()
            FROM Account
            WHERE Id IN :accountIds
            AND Rating = 'Hot'
        ];
        System.assertEquals(200, hotCount);
    }
}

Using @testSetup for Shared Apex Test Data

The @testSetup annotation creates common test data once for the test class. Each test method gets its own copy of that setup data, which keeps test methods isolated while reducing repeated setup code.

</>
Copy
@IsTest
private class AccountSetupExampleTest {
    @testSetup
    static void createData() {
        insert new Account(
            Name = 'Shared Test Account',
            AnnualRevenue = 500000
        );
    }

    @IsTest
    static void testAccountWasCreatedForThisMethod() {
        Account accountRecord = [
            SELECT Name, AnnualRevenue
            FROM Account
            WHERE Name = 'Shared Test Account'
            LIMIT 1
        ];

        System.assertEquals(500000, accountRecord.AnnualRevenue);
    }
}

Test.startTest() and Test.stopTest() in Apex Unit Tests

Test.startTest() and Test.stopTest() create a fresh set of governor limits for the code being tested. They are also important when testing asynchronous Apex, because queued asynchronous work runs when Test.stopTest() is called.

  • Place only the main action under test between Test.startTest() and Test.stopTest().
  • Create setup data before Test.startTest().
  • Perform assertions after Test.stopTest(), especially for future, queueable, batch, and scheduled Apex.

Running Apex Unit Tests in Salesforce

Apex tests can be run from Setup, Developer Console, Visual Studio Code with Salesforce extensions, or Salesforce CLI. The exact tool depends on the development workflow, but the goal is the same: run the tests, inspect failures, and review coverage before deployment.

  • Setup: use Apex Test Execution to select and run test classes.
  • Developer Console: use the Test menu to create and run a test suite.
  • Salesforce CLI: run tests from the terminal as part of local development or CI.
</>
Copy
sf apex run test --target-org my-org --test-level RunLocalTests --code-coverage --result-format human --wait 10

Use RunSpecifiedTests when deploying only selected test classes, RunLocalTests for tests created in the org excluding managed package tests, and RunAllTestsInOrg when all tests must be included.

Apex Test Data Isolation and SeeAllData

By default, Apex tests should create their own data. Relying on existing organization records makes tests unreliable because the result can change when admins, integrations, or other users change data. Avoid @IsTest(SeeAllData=true) unless a specific Salesforce object or metadata scenario requires it.

  • Create the Account, Contact, Opportunity, custom object, and related records needed by the test.
  • Use unique names or field values so the test does not depend on org data.
  • Do not assume that standard price books, profiles, queues, or records exist unless the test creates or queries them in a supported way.
  • Keep each test method independent so it can run alone or with other tests.

Common Apex Unit Test Mistakes to Avoid

  • Testing only for coverage: add assertions that prove the business result.
  • Testing only one record: include a bulk test for trigger and collection logic.
  • Using live org records: create test data inside the test class instead of depending on existing records.
  • Putting too much logic in triggers: move logic into handler or service classes so it is easier to test.
  • Ignoring negative cases: test invalid, missing, or boundary input when business rules require it.
  • Calling asynchronous logic without stopTest: use Test.stopTest() so queued work runs before assertions.

Testing Apex Unit Test FAQs

What are the 3 A’s of Apex unit testing?

The 3 A’s are Arrange, Act, and Assert. Arrange prepares test data, Act runs the Apex code being tested, and Assert verifies the actual result against the expected result.

What is the best approach to test an Apex trigger?

The best approach is to perform the DML operation that fires the trigger, then query or inspect the affected records and assert the expected result. Keep the trigger thin and place business logic in a handler or service class so both trigger behavior and core logic are easier to test.

How much Apex code coverage is required for deployment?

Salesforce requires at least 75% overall Apex code coverage for deployment or packaging, all selected tests must pass, and every trigger must have some test coverage.

Should Apex unit tests use SeeAllData=true?

Most Apex unit tests should not use SeeAllData=true. Create test records inside the test class so the test is repeatable and does not depend on existing organization data.

What are the main types of Apex unit test cases?

Useful Apex unit test cases include single-record tests, bulk tests, positive tests, negative tests, restricted-user tests, and asynchronous Apex tests. Together, they check behavior instead of only increasing coverage.

Apex Unit Test Editorial QA Checklist

  • Confirm each new Apex example uses @IsTest correctly and keeps test methods static.
  • Verify every test contains at least one meaningful assertion, not only DML execution.
  • Check that trigger examples include a 200-record bulk test where trigger logic is shown.
  • Confirm test data is created inside the test class or @testSetup method.
  • Review whether Test.startTest() and Test.stopTest() wrap only the action under test.
  • Run the examples in a Salesforce org or scratch org before publishing them as copy-paste code.