Best Practices For Apex Unit Tests

This article explores best practices for crafting Apex unit test code in Salesforce to optimize results. Setting up a development environment is a prerequisite for implementing these guidelines effectively.

To begin, I wish to clarify the motivation behind writing this article on best practices for Apex unit tests. The focus is on comprehending the purpose of conducting unit tests in Salesforce (and in other languages and delineating how to design them for maximum effectiveness.

What is the significance of Apex unit tests in Salesforce?

To meet the 75% coverage requirement imposed by SFDC for deploying my Apex Classes to PROD.”

I understand, folks; it can be exasperating, particularly when time is of the essence, and you find yourself at 74% coverage :'(

In reality, unit tests are the sole means by which a developer can alert the team if any modifications across the entire organization render their code nonfunctional.

Consider this scenario: you’ve crafted a beautiful Apex function, invoked by an Apex Trigger, responsible for updating all products associated with an Opportunity to mark the sold date when the opportunity reaches the “Closed / Won” stage.

It would be prudent to create an Apex Test class that:

  • Validates that when the stage changes, the products are appropriately updated.
  • Fails if the above condition is not met (and this aspect is crucial, as we’ll delve into in the next paragraph).

This is precisely why, if you hold a role as a technical leader or architect, implementing best practices for Apex Unit tests becomes imperative.

Ensure that you consistently employ assertions in your Salesforce Apex test classes.

In my perspective, incorporating assertions in unit test classes is essential. Executing the code is beneficial, but assertions are the means by which we GUARANTEE that the code we’ve written produces the expected results.

This enhances the robustness of the code.

Consider an example: Suppose we intend to utilize Salesforce for selling cars, and our objective is to update a custom field of the product (the sold car) with today’s date when referenced in an Opportunity that achieves a “closed/won” status.

The code might resemble the following (I’ve kept it simple for easy comprehension; additional checks may be required):

// Verify the Opportunity with the good stage
        List<Id> closedWonOpportunities = new List<Id>();
        for(Opportunity loopOpp : updatedOpportunities){
            if(loopOpp.StageName == 'Closed Won'){
                closedWonOpportunities.add(loopOpp.Id);
            }
        }
        // Get all the OLIs
        List<OpportunityLineItem> lineItems = [SELECT Id, Product2Id FROM OpportunityLineItem WHERE OpportunityId IN :closedWonOpportunities];

        List<Id> idOfProductsToUpdate = new List<Id>();
        for(OpportunityLineItem looOli : lineItems){
            idOfProductsToUpdate.add(looOli.Product2Id);
        }
        // Get the Products to update
        List<Product2> productsToUpdate = [SELECT Id , Sold_date__c FROM Product2 WHERE Id IN :idOfProductsToUpdate];
        // Update the Sold date to Today
        for(Product2 loopProductToUpdate : productsToUpdate){
            loopProductToUpdate.Sold_date__c = System.today();
        }
        update productsToUpdate;

Now, let’s see what the Test class looks like.
I added the data in the setup to make de code understandable.

@isTest
public with sharing class OpportunityHandlerTest {

    private static final String OPP_NAME = 'OPP_NAME';

    @TestSetup
    static void  makeData(){
        Account acc = new Account();
        acc.Name = 'TEST ACCOUNT';
        insert acc;

        Opportunity opp = new Opportunity();
        opp.Name = OPP_NAME;
        opp.AccountId = acc.Id;
        opp.StageName = 'Open';
        opp.CloseDate = System.today();
        insert opp;

        Product2 prod = new Product2();
        prod.Name = 'PROD NAME';
        insert prod;

        PriceBookEntry pEntry = new PriceBookEntry();
        pEntry.IsActive = true;
        pEntry.UnitPrice = 100;
        pEntry.Product2Id = prod.Id;
        pEntry.Pricebook2Id = Test.getStandardPricebookId();
        insert pEntry;

        OpportunityLineItem oli1 = new OpportunityLineItem();
        oli1.OpportunityId = opp.Id;
        oli1.Quantity = 1;
        oli1.TotalPrice = 200;
        oli1.PricebookEntryId = pEntry.Id;
        insert oli1;

    }

    @isTest
    static void ensureProductUpdated(){
        // Request the opportunity created in setup
        Opportunity testOpp = [SELECT Id, StageName FROM Opportunity WHERE Name = :OPP_NAME];        

        // retrieve the Opportunity line items
        List<OpportunityLineItem> lineItems = [SELECT Id, Product2Id FROM OpportunityLineItem WHERE OpportunityId = :testOpp.Id];
        List<Id> listOfProductIds = new List<Id>();
        for(OpportunityLineItem looOli : lineItems){
            listOfProductIds.add(looOli.Product2Id);
        }

        // retrieve the corresponding products and verify the sold date is null
        List<Product2> updatedProducts = [SELECT Id , Sold_date__c FROM Product2 WHERE Id IN :listOfProductIds];
        for(Product2 loopProductToUpdate : updatedProducts){
            Assert.isNull(loopProductToUpdate.Sold_date__c, 'The Sold date should be null');
        }

        // Now update the Opportunity
        testOpp.StageName = 'Closed Won';
        update testOpp;

        // request the products again
        updatedProducts = [SELECT Id , Sold_date__c FROM Product2 WHERE Id IN :listOfProductIds];
        
        // check that the product is correctly updated
        for(Product2 loopProductToUpdate : updatedProducts){
            Assert.areEqual(System.today() , loopProductToUpdate.Sold_date__c, 'The Sold date should not set to Today');
        }
    }
}

In the provided code, the following observations can be made:

  • Initially, we verify that the sold date is unset (indicating the opportunity is in the “Open” stage).
  • Following the update of the opportunity stage, we ensure that the sold date has been correctly set.

This approach serves to guarantee that if a developer introduces additional business logic on top of the existing logic, we can be completely certain that the originally implemented logic continues to function correctly.

The Salesforce Apex test philosophy: Establishing an isolated environment.


There’s something truly remarkable about the Salesforce Unit Test framework – when you execute a test, all the data you create, delete, or update exists in complete isolation from the data residing in the org where you’re running the tests.

What does this mean? It signifies that you can generate only the necessary data, manipulate it, and request it without the existing data influencing your test’s behavior.

This is truly incredible. When you’re developing in Java or other languages, achieving this kind of workflow is quite challenging. You often have to set it up manually, run a lightweight database like H2, but it’s not precisely the same database engine as in production. We don’t encounter these issues in Apex Unit Tests, and that’s a significant advantage.

Utilize Apex Test Data factories in your Salesforce development.

Employ Apex Test Data factories in your Salesforce development. As you delve into writing Apex unit tests, you’ll notice a frequent repetition of test data creation. Therefore, it’s strongly advised to centralize the creation of such test data.

Let’s explore how we can accomplish this.

In the section about assertions in this article, there’s a “@testSetup makeData” method that creates data. Now, let’s examine how to transform these creations to make them reusable.

Generate an Apex class for an Account Data Factory.

Create a class an put the creation of the Account in the static method:

public with sharing class Test_DF_Account {
    public static final String ACCOUNT_NAME = 'TEST ACCOUNT';
    public static Account createSimpleAccount(){
        Account acc = new Account();
        acc.Name = ACCOUNT_NAME;
        return acc;
    }
}

Repeat the process for establishing factories for Opportunities Pricebook, and so on.


I won’t go into the specifics of creating all the classes; I’ll focus on Opportunity and Opportunity Line Item, trusting that you understand how to create test data factory classes!

One crucial note: refrain from inserting the data in the factory. The idea is to generate an object that is prepared for insertion into the test class specific to the scenario you’ve identified.

ublic with sharing class Test_DF_Opportunity {
    public static final String OPP_NAME = 'OPP_NAME';

    public static Opportunity createSimpleOpportunity(Account paramAccount){
        Opportunity opp = new Opportunity();
        opp.Name = OPP_NAME;
        opp.AccountId = paramAccount.Id;
        opp.StageName = 'Open';
        opp.CloseDate = System.today();
        return opp;
    }

    public static OpportunityLineItem createOLI(Opportunity paramOpportunity, PricebookEntry pEntry){
        OpportunityLineItem oli1 = new OpportunityLineItem();
        oli1.OpportunityId = paramOpportunity.Id;
        oli1.Quantity = 1;
        oli1.TotalPrice = 200;
        oli1.PricebookEntryId = pEntry.Id;
        return oli1;
    }
}

Avoid utilizing the SeeAllData feature!


Let’s discuss a crucial feature: the @isTest(SeeAllData=True) annotation. This annotation exposes real data stored in the org to unit tests.

Using this feature is highly discouraged. The reason is, when you develop in a Sandbox, it has its own set of data. Once your test is moved to INTEGRATION, UAT, PRE-PROD sandboxes (depending on your release process), the data in these environments may differ from the data in the development environment.

Therefore, it’s strongly advised to avoid developing tests based on existing data. There’s a high likelihood that such tests will start to fail because of changes in the data.

Remember that Salesforce Organization-Wide Defaults (OWD) sharing rules apply in tests!

The sharing rules configured in your organization will take effect. Consequently, you might find it necessary to simulate the actions of another user, one with specific permissions, attempting to perform an action on the system.

To achieve this, employ the System.runAs method, which takes a user as a parameter and enables you to execute specific code. Refer to some examples here.

Maintain your Apex Unit tests as focused on the “unit” as much as possible.

Unit tests are purposefully crafted to assess the behavior of individual code units in isolation.

In this context, a unit refers to the smallest testable part of an application, typically a method or function. Adhering to the principle of small units ensures that each test concentrates on a specific functionality or feature. This approach not only streamlines the testing process but also facilitates easier debugging and maintenance.

An Apex unit test best practice, whenever possible, is to write one test per Apex method. This practice simplifies the identification of issues when a test fails, as it targets a specific piece of code.

If you need to test a method declared as private, no need to worry—simply annotate this method with @testVisible, and you’ll be able to access it directly in the Apex Test Class!

Leverage the Apex Test.StartTest and Test.StopTest methods.

Employing Test.StartTest and Test.StopTest methods proves exceptionally beneficial for resetting the governor limits during a test.Consider a scenario where you need to test code that approaches the Salesforce (SFDC) governor limits. Subsequently, you must execute additional SOQL queries to validate the results, but you’ve already reached the limit. In such cases, your test would fail, even though the code functions correctly outside of a test environment.As a best practice in Apex unit testing, incorporate the Test.StartTest and Test.StopTest methods strategically to assist with managing governor limits.Now, let’s explore how to structure the code for your test:

@isTest
public static void testAccountCreation(){
     // Create objects
     // Init things
     // Then start the test
     Test.startTest();
     // Run code to test
     // When all code to test hav been ran, stop the test
     Test.stopTest();
     // Verify things, do assertions
}