Skip to main content
Build the future with Agentforce at TDX in San Francisco or on Salesforce+ on March 5–6. Register now.

Create Specialized Tests

Learning Objectives

After completing this unit, you’ll be able to:

  • Write a test for a trigger that fires on a single record operation.
  • Execute all test methods in a class.
  • Explain the importance of permission-based testing.
  • Write permissions-based unit tests.

Test Apex Triggers

Before deploying a trigger, write tests to perform the actions that fire the trigger and verify expected results. Trigger tests are not exactly unit tests, as they don’t test what a method does, but what the trigger behavior is when a DML (data manipulation language) operation occurs. They are better categorized as integration tests.

It’s a best practice to have all the trigger logic enclosed in a class, typically called a trigger handler. It's there where you can actually write unit tests. Make sure to write unit tests for your trigger handler methods, on top of writing trigger integration tests as the one that follows.

Let’s test where if an account record has related opportunities, the AccountDeletion trigger prevents the record’s deletion.

  1. Open VS Code.
  2. Press Ctrl+Shift+P (Windows) or Cmd+Shift+P (macOS) to make the command palette appear.
  3. Enter Create Apex Trigger.
  4. Select SFDX: Create Apex Trigger.
  5. Enter AccountDeletionTrigger for the name.
  6. Replace the contents with the following code.
    trigger AccountDeletionTrigger on Account (before delete) {
      // Prevent the deletion of accounts if they have related opportunities.
      for(Account acct : [SELECT Id FROM Account
        WHERE Id IN (SELECT AccountId FROM Opportunity) AND
        Id IN :Trigger.old]) {
        Trigger.oldMap.get(acct.Id).addError(
          'Cannot delete account with related opportunities.');
      }
    }

  7. Click File | Save.
  8. Right-click the file you’re working on, then choose SFDX: Deploy Source To Org.

Now, let’s add a test method. The test method verifies what the trigger is designed to do (the positive case): preventing an account from being deleted if it has related opportunities.

  1. In VS Code create a new Apex class named AccountDeletionTriggerTests.
  2. Replace the default class body with the following.
    @IsTest
    private class AccountDeletionTriggerTests {
      @IsTest
      static void testDeleteAccountWithOneOpportunity() {
        // GIVEN
        // Create one account with one opportunity by calling utility method
        Account acct = TestFactory.getAccount('ACME', true);
        List<Opportunity> opps = TestFactory.generateOppsForAccount(acct.id, 1000.00, 1);
        insert opps;
        // WHEN
        Test.startTest();
          Database.DeleteResult result = Database.delete(acct, false);
        Test.stopTest();
        // THEN
        // In this case the deletion should have been stopped by the trigger,
        // so verify that we got back an error.
        Assert.isFalse(result.isSuccess());
        Assert.isTrue(result.getErrors().size() > 0);
        Assert.areEqual('Cannot delete account with related opportunities.',
        result.getErrors()[0].getMessage());
      }
    }
  3. Click File | Save.
  4. Right-click the file you’re working on, then choose SFDX: Deploy Source To Org.
  5. Click the Run Test button that appears on the testDeleteAccountWithOneOpportunity method.

Code Highlights

The test method first sets up a test account with an opportunity. Next, it deletes the test account, which fires the AccountDeletionTrigger trigger. The test method verifies that the trigger prevented the deletion of the test account by checking the return value of the Database.delete call. The return value is a Database.DeleteResult object that contains information about the delete operation. The test method verifies that the deletion was not successful and verifies the error message obtained.

Test for Different Conditions

One test method is not enough to test all the possible inputs for the trigger. We need to test some other conditions, such as when an account without opportunities is deleted. We also need to test the same scenarios with a bulk number of records instead of just a single record. Here is an updated version of the test class that contains the three additional test methods. Save this updated version of the class, deploy it and Run All Tests on the AccountDeletionTriggerTestsclass.

@IsTest
private class AccountDeletionTriggerTests {
  @IsTest
  static void testDeleteAccountWithOneOpportunity() {
    // GIVEN
    // Create one account with one opportunity by calling utility method
    Account acct = TestFactory.getAccount('ACME', true);
    List<Opportunity> opps = TestFactory.generateOppsForAccount(acct.id, 1000.00, 1);
    insert opps;
    // WHEN
    Test.startTest();
    Database.DeleteResult result = Database.delete(acct, false);
    Test.stopTest();
    // THEN
    // In this case the deletion should have been stopped by the trigger,
    // so verify that we got back an error.
    Assert.isFalse(result.isSuccess());
    Assert.isTrue(result.getErrors().size() > 0);
    Assert.areEqual(
      'Cannot delete account with related opportunities.',
      result.getErrors()[0].getMessage());
  }
  
  @IsTest
  static void testDeleteAccountWithNoOpportunities() {
    // GIVEN
    // Create one account with no opportunities by calling a utility method
    Account acct = TestFactory.getAccount('ACME', true);
    // WHEN
    Test.startTest();
      Database.DeleteResult result = Database.delete(acct, false);
    Test.stopTest();
    // THEN
    // Verify that the deletion was successful
    Assert.isTrue(result.isSuccess());
  }
  @IsTest
  static void testDeleteBulkAccountsWithOneOpportunity() {
    // GIVEN
    // Create accounts with one opportunity each by calling a utility method
    Account[] accts = TestFactory.generateAccountsWithOpps(200,1);
    // WHEN
    Test.startTest();
      Database.DeleteResult[] results = Database.delete(accts, false);
    Test.stopTest();
    // THEN
    // In this case the deletion should have been stopped by the trigger,
    // so check that we got back an error.
    for(Database.DeleteResult dr : results) {
      Assert.isFalse(dr.isSuccess());
      Assert.isTrue(dr.getErrors().size() > 0);
      Assert.areEqual(
        'Cannot delete account with related opportunities.',
        dr.getErrors()[0].getMessage());
    }
  }
  
  @IsTest
  static void testDeleteBulkAccountsWithNoOpportunities() {
    // GIVEN
    // Create accounts with no opportunities by calling a utility method
    Account[] accts = TestFactory.generateAccountsWithOpps(200,0);
    // WHEN
    Test.startTest();
      Database.DeleteResult[] results = Database.delete(accts, false);
    Test.stopTest();
    // THEN
    // For each record, verify that the deletion was successful
    for(Database.DeleteResult dr : results) {
      System.assert(dr.isSuccess());
    }
  }
}

Test Permission-Based Scenarios

Permission-based testing can be the most complex testing pattern of all. In part, that’s because permissions can be confusing, and in part it’s because a good set of permissions tests uses both positive and negative test patterns. To write a permissions test, you need to generate not only test data, but also one or more test users. Once you have those, you can write both positive and negative tests and run them as your test users with or without specific permissions.

This video offers an introduction to permission-based testing.

Permission-Based Testing Pattern

Let’s take a look at the pattern for permission tests.

  1. Generate or load your test data.
  2. Create users with appropriate permission sets.
  3. Start a System.runAs(user) block.
  4. Execute your negative or positive test inside the System.runAs(user) block.

The permission set testing case is the one case where you do not need to create your own test data. A permission set is a record detailing which permissions are granted. These permission set records are effectively metadata and not data. As they are part of your organization configuration, you use existing permission sets when testing. Thus, in your tests, you only need to create a test user and assign an existing permission set to that user.

The package you installed contains a permission set called Private_Object_Access and a custom object whose default sharing is set to private. With that in place, let's look at what a permission-set test looks like.

  1. Open VS Code.
  2. In the Explorer sidebar, right-click the folder classes, then choose SFDX: Create Apex Class.
  3. Name the new class PermissionsTests, and accept the default directory.
  4. Replace the contents of the class with the following code.
    @IsTest
    private class PermissionsTests {
      @TestSetup
      static void testSetup() {
        // GIVEN
        Account acct = TestFactory.getAccount('No view For You!', true);
        Private_Object__c privateObj = new Private_Object__c(Account__c = acct.id, Notes__c = 'foo');
        insert privateObj;
      }
      @IsTest
      static void testNegativePermissionSet() {
        // GIVEN
        User userNew = TestFactory.generateUser('Standard User');
        System.runAs(userNew) {
          // WHEN
          Test.startTest();
            Private_Object__c[] privateObj = [SELECT Id, Account__c, Notes__c FROM Private_Object__c];
          Test.stopTest();
          // THEN
          Assert.areEqual(
            0,
            privateObj.size(),
            'A user without the permission set shouldn\'t see any records');
        }
      }
    }
  5. Click File | Save.
  6. Right-click the file you’re working on, then choose SFDX: Deploy Source To Org.
  7. Click the Run Test button that appears on the testNegativePermissionSet method.

Code Highlights

The test method in our code above demonstrates that users without the permission set cannot see the Private_Object__c records.

In our @TestSetup method, we’re creating the Private_Object__c record associated with an account. Because the sharing model is set to private, only the system can see this record. When we execute the actual test method, we create a new user and execute the query as that user. That results in the new user being unable to see the Private_Object__c record.

Resources

Share your Trailhead feedback over on Salesforce Help.

We'd love to hear about your experience with Trailhead - you can now access the new feedback form anytime from the Salesforce Help site.

Learn More Continue to Share Feedback