Write and execute tests for triggers, controllers, classes, flows, and processes using various sources of test data.

After studying this topic, you should be able to:

  • Identify how to write tests for Apex code as well as flows and process
  • Determine the different options for generating test data that can be used in test methods
  • Define the different sources of test data for executing tests and/or options for creating them
  • Describe the different tools or platforms that can be used for executing tests in Salesforce
  • Identify best practices or considerations when writing unit tests in Salesforce

Table of Contents

  • Writing Unit Tests
  • Testing Classes and Triggers
  • Testing Apex Controllers
  • Testing Governor Limits
  • Testing Permission Set Groups
  • Testing Code as Another User
  • Testing Flows & Processes
  • Test Data Sources
  • Using @testSetup for Test Data
  • Using Test Utility Classes
  • Using Static Resources as Test Data
  • Pricebook Test Data
  • Using Existing Data as Test Data
  • Executing Tests
  • Apex Test Execution Page
  • Execute Tests in Developer Console
  • Execute Tests in Visual Studio Code
  • Execute Tests using an API
  • Managed Packages and Unlocked Packages
  • Other Concepts & Best Practices
  • Unit Test Examples

Introduction

  • Apex testing is part of the application lifecycle management in Salesforce - it is a technical requirement before Apex code can be deployed to a production org. The following section discusses:
    • Writing tests for Apex classes, triggers, controllers, as well as flows and processes
    • Different sources for test data that can be used in unit tests, including different tools available for executing tests
    • Salesforce provides a robust testing framework, making it easy for developers to manage and run tests, facilitating the building of optimized, stable Salesforce applications

  • Apex Testing Overview:
    1. Write Unit Tests: write unit tests for Apex classes and triggers for error-free code
    2. Run Unit Tests: execute a unit test or test suite for running test classes together
    3. Generate Code Coverage: generate test coverage for code every time a test is run
    4. Check Test Results: check whether tests succeed or fail in the Developer Console

Writing Unit Tests

  • Test Data Factory: test data can be set up using a public test utility class, a method defined using @testSetup, or directly in test methods
  • Apex classes: apex classes containing code are called directly by test methods for testing, test methods are used to verify various scenarios in order to ensure functionality
  • Custom Controllers, Controller Extensions: query parameters can be set and used in unit tests for custom controllers and controller extensions
  • Flows & Processes: can also be tested to increase flow test coverage
  • DML Operations, Apex Triggers: triggers can be tested by including DML operations in test methods

  • Test classes and methods must be annotated with @isTest - they can be either public or private
  • Test methods should be static, return no values (void), and accept no parameters
  • Test data should be available to the test methods, possibly set up using test class methods annotated with @testSetup
  • An exception is thrown when using assert methods if a test is failed and the actual result does not match the expected result
  • Unit tests should execute the Apex method or trigger that is being tested
  • To test triggers, include the DML operations in test methods - the triggers will be called
  • Verify test results using System.assert() methods

  • Sample typical structure of an Apex Test class shown below
    • @isTest is used to define the Apex class as a test class
    • @testSetup defines the method as a method used for creating test data
    • @isTest defines methods as a test method - not all methods in a test class have to be test methods
    • Test methods must be static, must be void, and not return any value
    • A naming convention such as ending in __Test is advised to easily identify test classes and list them next to the classes they are testing
@isTest
private class myApexClass__Test {

    @testSetup
    private static void setupTestData() {
        // create records to use in the test methods
    }

    @isTest
    private static void myTestMethod() {
        // test business logic in an Apex method
        // System.assertEquals(expected, actual, msg);
    }
}
  • All unit tests follow the same basic structure:
    1. Create valid test data for testing
    2. Execute the methods to be tested
    3. Verify the expected results using assertions
  • Code Coverage Percentage, CCP = (number of covered lines) / (number of covered + uncovered lines)
    • Excludes: code comments, class name definition, lines that only contain brackets, System.debug statements

Testing Classes and Triggers

  • Unit tests should include both positive and negative test cases to verify both positive and negative code behavior:
    • Positive Case: uses valid inputs within the governor limits
    • Negative Case: uses one or more invalid inputs, or exceeds governor limits, to verify the code doesn’t allow normal processing in those conditions

Example Apex Class

  • Automatically creates a sales order for each Closed Won opportunity and updates a field on the opportunity
public class OpportunityUpdateClass {
    public static void processRecords (List<Opportunity> paramOpps) {
        List<Sales_Order__c> insertSales = new List<Sales_Order__c>();
        List<Opportunity> updateOpps = new List<Opportunity>();
        for (Opportunity o : paramOpps) {
            if (o.StageName == 'Closed Won') {
                Sales_Order__cs = newSales_Order__c(Name=o.Name+' Order', 
                                                    Status__c='New', 
                                                    Opportunity__c=o.Id);
                // Add the records to insert and update in list collections
                insertSales.add(s);
                
                o.Notes__c = 'A sales order has been created';
                updateOpps.add(o);
            }
        }
        // Insert the sales orders and update the opportunities
        insert insertSales;
        update updateOpps;
    }
}

Example Positive Test Case

@isTest
private class OpportunityUpdateClassTest {
    @isTest
    public static void positiveTest() {
        List<Opportunity> opportunities = newList<Opportunity>();
        // Create test data
        for (Integer i = 1; i <= 300; i++) {
            Opportunity opp = new Opportunity(Name='Test Opportunity '+i,
                                              CloseDate=Date.newInstance(2020, 07, 15), 
                                              // Valid input
                                              StageName='Closed Won');
            opportunities.add(opp);
        }
        insert opportunities;
        
        // Execute the method to test
        OpportunityUpdateClass.processRecords(opportunities);
        
        // Verify test results
        for (Sales_Order__cs : [SELECT s.Name, s.Opportunity__r.Name, s.Status__c,
                                       s.Opportunity__r.Notes__c
                                  FROM Sales_Order__c]) {
            System.assertEquals(s.Opportunity__r.Name + ' Order', s.Name);
            System.assertEquals('New', s.Status__c);
            System.assertEquals('A sales order has been created', s.Opportunity__r.Notes__c);
        }  
    }
}

Example Negative Test Case

@isTest
private class OpportunityUpdateClassTest {
    @isTest
    public static void negativeTest() {
        // Create Test Data
        List<Opportunity> opportunities = newList<Opportunity>();
        for (Integeri = 1; i <= 300; i++) {
            Opportunityopp = newOpportunity(Name='Test Opportunity '+i,
                                            CloseDate=Date.newInstance(2020, 07, 15),
                                            // Use invalid input
                                            StageName='Closed Lost');
            opportunities.add(opp);
        }
        insert opportunities;
        // Execute the method to test
        OpportunityUpdateClass.processRecords(opportunities);
        // Verify that the method did not process any sales orders
        for (Opportunity o : [SELECT Id, 
                                     Notes__c, 
                                     (SELECT Id 
                                        FROM Sales_Orders__r) 
                                FROM Opportunity 
                               WHERE Id IN :opportunities]) {
            System.assert(o.Sales_Orders__r.size() == 0);
            System.assertNotEquals('A sales order has been created', o.Notes__c);
        }
    }
}

Example Apex Trigger

  • Automatically assigns the account of a contact based on the serial number of a service level agreement
trigger ContactTrigger on Contact (before insert) {
    
    List<String> agreements = newList<String>();
    List<Account> listAccounts = newList<Account>();
    Map<String, Account> mapAccounts = newMap<String, Account>();
    
    if (Trigger.isBefore && Trigger.isInsert) {
        for (Contact c : Trigger.New) { 
            // Collect serial numbers to use in querying the accounts
            agreements.add(String.valueOf(c.SLA_Serial_Number__c)); 
        }
        // Query the accounts and create a serial number mapping
        listAccounts = [SELECT Id, 
                               SLA_Serial_Number__c 
                          FROM Account 
                         WHERE SLA_Serial_Number__c IN :agreements];
        for (Account a : listAccounts) { 
            mapAccounts.put(a.SLA_Serial_Number__c, a); 
        }
        if (!mapAccounts.isEmpty()) {
            for (Contact c : Trigger.New) {
                // Use the map to retrieve the account Id to assign to the contact
                c.AccountId = mapAccounts.get(String.valueOf(c.SLA_Serial_Number__c)).Id;
            }
        }
    }
}

Example Unit Test for Apex Trigger

  • Sample test class for testing the functionality of the ContactTrigger Apex trigger
@isTest
public class ContactTriggerTest {
    @testSetup
    static void createTestData() {
        // Set up test data
        Account a = newAccount(Name='Acme',
                               SLA_Serial_Number__c='1234567890');
        insert a;
    }
    @isTest
    static void testMapping() {
        // Create a contact record to invoke trigger
        Contact c = newContact(LastName='Smith', 
                               SLA_Serial_Number__c='1234567890');
        insert c;
        Account a = [SELECT Id, 
                            Name
                       FROM Account
                      WHERE Name = 'ACME'
                      LIMIT 1];
        Contact r = [SELECT Id, 
                            AccountId
                       FROM Contact
                      WHERE Id = :c.Id];
        // Verify test result
        System.assertEquals(a.Id, r.AccountId);
    }
}

Testing Apex Controllers

  • Controller extensions and custom controllers should be covered by unit tests
    • Query parameters can be set which then can be used in the tests
    • Example below calls method and passes parameters to test a Visualforce controller
 @isTest
 public static void testController() {
    PageReference pageRef = Page.success;
    Test.setCurrentPage(pageRef);
    
    CustomControllercontroller = newCustomController();
    String nextPage = controller.save().getUrl();
    // Verify that error handling is working as expected
    System.assertEquals('/apex/failure?error=noParam', nextPage1);
    
    // Pass parameters to the Visualforce page
    ApexPages.currentPage().getParameters().put('company', 'acme');
    controller2 = newtheController();
    controller2.setLastName('lastname');
    controller2.setFirstName('firstname');
    controller.setEmail('user@acme.com');
    String nextPage2 = controller2.save().getUrl();
    nextPage = controller.save().getUrl();
    // Verify that the success page is working as expected
    System.assertEquals('apex/success', nextPage2);
    
    System.assertEquals('/apex/success', nextPage);
    List<Lead> leads = [SELECT Id, Email FROM Lead WHERE Company = 'acme'];
    // Verify that the user was successfully created
    System.assertEquals('user@acme.com', leads[0].Email);
}

Testing Governor Limits

  • Unit tests or Apex code executed in test methods are subject to governor limits
    • Start and Stop Test:
      • Test.startTest() and Test.stopTest() methods can only be called once in each test method
      • Can be used in conjunction with Limits methods to test governor limits by determining the proximity of Apex code to reaching the governor limit
    • Actual Code Testing: Test.startTest and Test.stopTest() mark the point where the testing code actually begins and ends
      • Code before actual test begins should be used to set up the scenario - such as initializing variables and creating test data
    • Fresh Governor Limits
      • Any code executed before stopTest and after startTest are assigned a new set of governor limits
      • Ex: if 99 SOQL queries are executed before startTest, 100 queries can be performed after startTest. But, only one more qery allowed after stopTest before exceeding query limit
    • Limits Methods for Testing Governor Limits: used to return the amount of resource consumed in a transaction and the general total amount of resource available pertaining to governor limits
      • Method Versions: Each Limit has two versions. One for determining amount of resource already consumed in current transaction, the other contains the limit IE total amount of resource that can be used
      • Available Resource: By using those two version in combination, the remaining amount of resource can be calculated
    • Example Method: getCallouts method returns the number of callouts already processed, and getLimitCallouts method returns the total number of callouts available. Subtracting getCallouts from getLimitCallouts returns the balance
    • Example below shows two sets of governor limits in the unit test below - one inside the boundaries of startTest and stopTest, one outside
@isTest
private class StartStopClassTest {
    @isTest
    static void doUnitTest() {
        Test.startTest();
        Contactcon = newContact();
        con.LastName = 'SampleContact';
        insertcon;
        System.assertEquals(1, Limits.getDmlStatements());
        
        Test.stopTest();
        
        System.assertEquals(0, Limits.getDmlStatements());
    }
}

Testing Permission Set Groups

  • Testing Permission Set Groups: When testing permission set groups, the calculatePermissionSetGroup() method can be used to recalculate the aggregate permissions in the permission set group

Testing Code as Another User

  • Salesforce allows executing code in the context of a specified user for testing by using the System.runAs method
    • System Context: Apex runs in system mode by default, meaning permissions and record sharing of the current user are not considered
    • User Context: System.runAs method allows test methods to run in the context of an existing user or a new user
    • Record Sharing: record sharing of the user specified in the System.runAs method is enforced
    • User & Field-Level Permission: user permissions and field-level permissions in System.runAs method are not enforced
    • Run as Method Availability: System.runAs method can only be used in test methods
    • User License: User license limits are ignored, so any number of users can be created programmatically
    • DML Limit: A call to runAs counts against total number of DML statements
    • Nested Methods: runAs methods can be nested, or another runAs method can be contained in a runAs method
    • Mixed DML: Mixed DML operations can be performed by enclosing them within the runAs block
    • Setup Objects: setup objects can be inserted or updated together with other sObjects using the runAs block
  • System.runAs enables code to run in the context of a specified user to test code behavior and record access to the user

Testing Flows & Processes

  • Some implications for writing unit tests for Flows:
    • Retrieve the Output: output variables can be retrieved from a flow by using the getVariableValue() method of the Flow.Interview object
    • Start the Flow: the autolaunched flow can be run by calling the start() method of the Flow.Interview object
    • Use a Map: A variable of Map data type is used to set input parameters for a flow
    • Verify the Output: The flow output is used to determine whether or not the flow is working as expected
    • Increase coverage: Invoking the flow in a test class increases the flow test coverage
  • Sample Flow Configuration: Below is a sample configuration of an autoluanched flow that accepts a Project record and creates a Project Log record for the given project
  • Example Unit Test for a Flow: The following shows an example unit test to test a flow that automatically creates a log record when a project record is created
  • Testing Flows with External Services
    • Apex unit tests can also be created for flows that use an invocable action with External Services. The HttpCalloutMock interface can be used to simulate the callout to the external service endpoint
    • The flow test class sets up the HTTP callout mock for the external service action, creates and runs the flow interview, and then asserts the actual flow output parameters with the expected test values

  • Some implications for writing a unit test for processes
    • Invoke Process: writing the test class involves creating an Apex test method that invokes the process
    • Verify Results: the results produced by the invoked process is validated to ensure that the process is working as expected
    • Increase Coverage: invoking the process in a test class increases the flow test coverage
  • Example Unit Test for a Process: Below tests a process which automatically updates a field value on the Project record to Assigned when a project manager, which is a lookup field, is assigned to the project

  • Calculating the Flow Test Coverage: minimum test coverage percentage required for flows and processes is independent from the Apex code coverage requirement
    • Minimum test coverage percentage can be defined in Process Automation Settings in Setup when the Deploy processes and flows as active option is enabled
      • Only available in production orgs since active flows and processes can always be deployed as active in sandbox orgs
    1. Determine Active Flows/Processes: find the actual count via the Tooling API
    2. Run all tests in the org
    3. Determine covered Flows/Processes: number of active autolaunched flows and processes that have test coverage via Tooling API
    4. Calculate Test Coverage: Divide the number of active autolaunched flows that have test coverage by the total
      • Note, calculating this requires querying the Flow and FlowTestCoverage objects, which belong to the Tooling API
  • In example below, a total of 20 active autolaunched flows and processes were found when running the query below

  • Implications of deploying flows and processes:
    • Deployed as Inactive: by default in production orgs
    • To Deploy as Active: Deploy processes and flows as active option must be enabled in Process Automation Settings
    • Flow Test Coverage is independent from the Apex code coverage requirement and is set manually in Setup
    • Screen Flows Excluded: flow test coverage requirement applies to processes and flows, but does not apply to flows that have screens

Test Data Sources

  • Test Data Sources Overview: several options for providing test data to test classes:
    • Test Setup method: Create common test data for every test method in the class
    • Pre-Existing Data Access: isTest(SeeAllData=true) annotation
    • Programmatic Data Access: create test data directly in the test method
    • Test Utility Class: create Test Data Factory public utility class with reusable class
    • Price Book Entries: retrieve standard price book ID to create price book entries
    • Static Resources: Use static resources with Test.loadData method to populate test records
  • Creating Test Data Programmatically: when test data is created through a test method or a test data factory class, the following should be considered
    • Data is temporary: data created programmatically does not persist in the database
    • Data is rolled back: test records are rolled back when the unit test finishes execution
    • No need to delete data
    • No Data Committed: Salesforce records created in test methods are not committed to the database
    • No data persistence: updates and deletions to existing records don’t persist after test execution

Using @testSetup for Test Data

  • Test Setup methods: Used to programmatically create test records once, then the records can be accessed in every test method in the test class
    • Create Test Data: test setup method, or method annotated with @testSetup is used to create test data, which becomes available to test methods in the test class
    • Reuse test data: common test data can be created which avoids the need to re-create the records in each test method
    • Setup is run first: test setup method is executed first before any test method in the test class by the testing framework
    • Test data is reset: changes to records created in @testSetup method are rolled back after each test method finishes
    • Test data access: records that are created become available to all test methods in the test class
    • Test data rollback: if a test method makes changes to the records, such as record field updates or deletions, those changes are rolled back after each test method finishes
    • Original state: original unmodified state of the records are always access byed the next executing test method
    • Method syntax: test setup methods have no arguments and no return value
    • Test setup support: test setup methods are not supported if the test class or test method has access to existing data using @isTest(SeeAllData=true)
    • Method limit: there is a limit of one test setup method only per test class
    • Test setup error: errors encountered in the test setup method will cause the entire test to fail, so no test methods will be executed
  • Following example unit test tests and Apex trigger functionality

Using Test Utility Classes

  • Test Utility Classes: common test utility classes are public test classes that contain reusable code for test data creation
    • Temporary data: test data created programmatically does not persist in the database
    • Limit exclusion: public test utility classes are annotated with @isTest to exclude it from the org’s code size limit and execute in test context
    • Class availability: they can only be called by test methods and not by non-test code
    • Avoid duplication: duplication of test data creation or helper methods are avoided, making code easier to maintain
    • Data setup: methods in a public utility class can be called by any test method in Apex classes to set up test data before running the test
    • Access modifier: methods in a public utility class should be declared as public or global to be visible to test classes
  • Below sample test class uses the test utility class to create records used for the test

  • Below is a sample test utility class with a method that creates and returns test accounts

Using Static Resources as Test Data

  • Create data from static resources: static resources and Test.loadData method can be used to populate data in test methods without writing as much code
    • Test data file: test data should be prepared and saved in a CSV file
    • Static resource file: CSV file should be uploaded in the org as a static resource
    • Method parameters: Test.loadData method requires the Object type token and the name of the static resource
    • Test data file format: A MIME type of text/csv is assigned to the static resource once it is uploaded, and is used to determine the default action when the resource is fetched
    • Method execution: Test.loadData is called inside the test method that requires it
    • Return value: Test.loadData method returns a list of sObjects for each record inserted
    • Method prerequisite: static resources should be created before calling the Test.loadData method
  • Following shows details of a CSV file uploaded in Static Resources:

  • When using static resources for loading test data, it is possible to relate records by using fake Ids on the record Id and lookup field Id

  • Using a Static Resource for Test Data: below shows a sample test method that loads test data from a static resource

Pricebook Test Data

  • Creating Pricebook Entries: Creating price book entries with a standard price requires the ID of the standard price book
    • Standard Price Book Id: The getStandardPricebookId method returns the ID of the standard price book in the org regardless of whether or not the test can query org data
    • Test Price Book Entries: The getStandardPricebookId method is used to get the standard price book ID so that price book entries can be created in tests
  • This example shows how the getStandardPricebookId() method is used to create price book entries for test data

Using Existing Data as Test Data

  • Notes:
    • Existing data can be accessed via SOQL queries, but it is not recommended as the data will be org-dependent
    • So, its recommended to store data in static resources or create data programmatically
  • Access to pre-existing data: there are pre-existing org data that are always accessible to test classes
    • No Access by Default: test classes do not have access by default to pre-existing data with some exceptions
    • Access to Org Data: objects that are used to manage the org, or metadata objects, such as Users, Profile, Organization and Record Type can be accessed in unit tests
  • Example of accessing pre-existing data:

  • SeeAllData: Pre-existing data can be accessed by annotating the test class or method with @isTest(SeeAllData=true)
    • Class Annotation Coverage: @isTest(SeeAllData=true) applies to all test methods in a test class
    • Class vs Method Annotation: If a test method is annotated with @isTest(SeeAllData=false) and the containing class is annotated with isTest(SeeAllData=true), org access is not restricted for that method
  • Test class below illustrates the different results of two annotated test methods

Executing Tests

  • Executing Unit Tests: following tools can be used to run unit tests in Salesforce:
    • Apex Test Execution
    • Apex Classes page (Run All Tests)
    • Developer Console
    • Visual Studio Code
    • API (SOAP API, Tooling REST API, Etc)
  • Grouping Unit Tests: Salesforce allows the following groupings of unit tests to be run:
    • Single Class: Some or all methods in a specific class
    • Test Suite: Predefined suite of classes can be run
    • Multiple Classes: Some or all methods in a set of classes
    • All Tests: All unit tests in an org can be run
  • Test Options for Change Sets: 4 types of test options are available when validating or deploying a change set:
    • Default
      • Sandbox: No tests executed
      • Production: All local tests are executed (Run Local Tests)
    • Run Local Tests: All tests in the org are run except tests contained in installed managed packages
    • Run All Tests: All tests in the org are run including tests contained in installed managed packages
    • Run Specified Tests: Each Apex component invoked in the specified test classes must have at least 75% code coverage
  • Running Specified Tests: Selected tests, IE specified tests, is a testing option. This allows specifying the test classes to execute. In this mode, the requirements are as follows:
    • Deployment Requirement: if the code coverage of any Apex component is less than 75, the validation will fail and deployment is not allowed to proceed
    • Running Specified Tests: running a subset of test results to perform quicker test runs. Specified tests can also be run in the Metadata API and are exposed to tools that are based on Metadata API, such as the Ant Migration Tool
    • Individual Code Coverage: Code coverage is computed individually for each Apex class and trigger and is different from the overall code coverage percentage
    • Specifying Apex Classes: Only test classes can be specified, not individual test methods. When selecting Run specified tests in the UI, for example, names of the test classes are specified in a comma-separated list
    • The following features are not supported in unit tests:
      • Outbound Email: Unit tests cannot send outbound emails. When a unit test includes Apex code that sends an email message, it may run successfully, but no email is actually sent out from the org
      • Apex Callouts: Unit tests cannot perform HTTP callouts. In order to test Apex code that performs callouts, mock responses are created using the HttpCalloutMock interface

Apex Test Execution Page

  • The following lists the different options or modes available when running tests in the Apex Test Execution page:
    • All or Individual Classes: an option to select either all or individual Apex test classes to run
    • Skip Code Coverage: an option to skip computation of code coverage during the test execution
    • Parallel Test Execution: selected tests to run are placed in an Apex Job queue and run in parallel by default
    • Asynchronous Execution: tests that are executed in the Apex Test Execution page are run asynchronously
    • My Namespace Tests: Tests that exist locally in the org can be filtered by using the [My Namespace] option in the drop-down
    • All Namespaces: option [All Namespaces] displays all test classes regardless of the namespaces they belong to
    • Managed package Tests: tests from installed managed packages can be filtered by selecting the managed package’s corresponding namespace provided in the drop-down list
    • Code Coverage Details: After tests have completed, code coverage details can be viewed by clicking on the Developer Console button
    • Test Execution Limit:
      • Production: No more than 500, or 10x the number of test classes, can be executed every 24 hours
      • Sandbox/Developer Editions: No more than 20x the number of test classes
    • Test Execution History: test results can be viewed by clicking on the View Test History link button above the test results table
  • A list of the unit tests that are executed are displayed in a table which shows the status, class and result of the test

  • The Select Tests button opens the window for selecting which test classes to execute
    • Skipping code coverage can be enabled in the Test Classes window

  • Skipping Code Coverage: is allowed by enabling the setting before the test execution
    • Coverage computation skipped: skipped for all executed tests until the Developer Console is closed, or when the Skip Code Coverage setting is enabled
    • Skip Code Coverage in Apex Test Execution: the skip code coverage option is available in the Apex Test Execution page via the Test Classes window for selecting tests to run
    • Skip Code Coverage in Developer Console: checkbox is available in the Specify Run Parameters dialog box, which can be opened via the Settings button after clicking on the Test > New Run menu option in the Developer Console
    • Speed Up Test Results: skipping code coverage will speed up test execution and return the results of the tests faster - in this mode, no code coverage info is provided

Execute Tests in Developer Console

  • Test Suite in Developer Console: Developer Console can be used to execute all test methods in atest class or run all tests in an org
    • Unit tests can be grouped together in a test suite to execute specific test methods, either from a single class or from multiple classes
  • A test suite is a collection of Apex test classes that is grouped and run together. This enables running the same subset of tests whenever required.
    • Manage Test Suites: Suite Manager is used to create, edit, delete, or rename test suites
    • Test Suite Classes: Suite Manager is used to manage which Apex test classes are contained in a test suite
    • Test Suite for Deployment: a test suite can be created which is run every time to prepare for deployment or Salesforce release updates

Execute Tests in Visual Studio Code

  • Visual Studio Code can also be used to execute Apex tests
    • Software Requirements: Salesforce CLI and the Salesforce Extension Pack needs to be installed
    • Execution Options: tests can be executed from either the Apex Test sidebar, or code editor, which contains actual code of the test classes
    • Test Methods: VS Code allows running a single method only, or all test methods in a test class, or all tests that are available
      • Single Test method: hover on the name of a test method and click on the Run Single Test button
      • All Test methods in a Class: hover on the name of a test class and click on the Run Tests button
      • All Tests in All Classes: hover on top of the Apex Tests area and click on the Run Tests button
  • Apex Test Sidebar automatically detects Apex tests and displays the test classes and methods in a list

Execute Tests using an API

  • Tests can be run synchronously or asynchronously using SOAP API calls and objects
    • All or Selected Test Classes
    • Asynchronous Testing: the ApexTestQueueItem object can be used to add tests to an Apex job queue and run them asynchronously
    • Synchronous Testing: tests can be run synchronously by using the runTests() call provided by the SOAP API
    • Tooling API: can be used to run tests
    • Checking Results: ApexTestResult object can be used to check the result of an Apex test method execution
  • Following shows sample code that runts tests asynchronously and returns a job identifier

  • Below shows sample code for retrieving test results based on a job identifier

  • The Tooling REST API can be used to perform asynchronous or synchronous tests. The following shows two ways of requesting an asynchronous test execution.

Managed Packages and Unlocked Packages

  • Managed Packages and Unlocked Packages: when running the SFDX report command to retrieve details of a package version for second-generation managed or unlocked packages, the output can also include code coverage details
    • Managed Packages: 2nd-generation managed packages, or also know as managed 2GP, are used for distributing apps on AppExchange using Salesforce DX
    • Unlocked Packages: unlocked packages are related or organized metadata that are packaged together using the package development model to build apps that are versionable, easy to maintain, update, install and upgrade
    • Code Coverage: Code coverage details included in the report can be used to identify which Apex classes in the package fail to meet the required code coverage percentage
  • Unlocked Packages: The force:package:version:report command is used to retrieve details of a package version. To include code coverage details, the --verbose flag can be used

Other Concepts & Best Practices

  • Accessing Private or Protected Members Outside a Test Class: private or protected members can be exposed to test classes by using an annotation
    • Using @TestVisible: @TestVisible annotation allows private or protected members (methods, variables, inner classes) to be visible to a test class

  • Apex Testing with sObject Error Methods: The sObject class contains error methods that can be used in Apex testing to track and obtain errors without performing any DML operation
    • sObject.addError(): method can be used to dynamically add errors to specific fields
    • sObject.hasErrors(): method can be used to determine if an object instance contains any errors
    • sObject.getErrors(): method can be used to retrieve a list of errors for a specific object instance
// sObject.addError()
String field = 'Site';
String msg = 'website does not exist.';
account.addError(field, msg);

// sObject.hasErrors()
Boolean has = account.hasErrors();
System.assertEquals(true, has);

// sObject.getErrors()
Integer size = account.getErrors().size();
System.assertEquals(1, size);
  • SObject Error Methods Example: shows an example of using of the error methods in testing Apex code

  • Deployment Requirements Overview: tests should cover as many lines of code as possible. Note the following before deploying Apex code:
    • Successfully Compiled Code: Apex classes and triggers must be compiled successfully and not throw any uncaught exceptions or limit exceptions
    • Compliant Code Coverage: Overall code coverage in an org must be at least 75% for successful deployment
    • Code Coverage Exclusion: Test classes and test methods are excluded from the code coverage requirement
    • Optimize Unit Tests: test methods should be optimized for every possible scenario and use case of an application
    • Apex Trigger Coverage: each Apex trigger must at least have one line of code covered in the test

  • Best Practices When Writing Test Cases
    • Unit tests should set up conditions for all testing scenarios, such as normal, unexpected, boundary, and bad input values
      • Multiple Test Cases: Unit tests should take into consideration positive, negative, and user-restricted code behavior
      • Non-Persistent Data: Unit tests do not commit changes, so test data doesn’t need to be deleted nor changes undone after use
      • Create Test Data: Unit tests should create their own test data and not depend on existing data in the org
      • Verify Results: System.assert() methods should be used to verify whether the expected results are met or not
      • Single & Bulk Tests: Create unit tests that test code on a single record as well as on multiple records
      • Test Suites: Tests should be added to a test suite when a new bug is found in the code to simplify regression testing
      • Focus Tests: Unit tests should test only one aspect of code at a time to make it easier to understand the purpose
      • Test-Driven: Test-driven development or creating unit tests before writing new code can help to understand how code will be used

Unit Test Examples

Reference FoF Slides