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.
- Open VS Code.
- Press Ctrl+Shift+P (Windows) or Cmd+Shift+P (macOS) to make the command palette appear.
- Enter
Create Apex Trigger
. - Select SFDX: Create Apex Trigger.
- Enter
AccountDeletionTrigger
for the name. - 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.'); } }
- Click File | Save.
- 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.
- In VS Code create a new Apex class named
AccountDeletionTriggerTests
. - 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()); } }
- Click File | Save.
- Right-click the file you’re working on, then choose SFDX: Deploy Source To Org.
- 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 AccountDeletionTriggerTests
class.
@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.
- Generate or load your test data.
- Create users with appropriate permission sets.
- Start a
System.runAs(user)
block. - 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.
- Open VS Code.
- In the Explorer sidebar, right-click the folder classes, then choose SFDX: Create Apex Class.
- Name the new class
PermissionsTests
, and accept the default directory. - 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'); } } }
- Click File | Save.
- Right-click the file you’re working on, then choose SFDX: Deploy Source To Org.
- 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.