Read more at jamessimone.net, or return to the homepage





Testing Custom Permissions

Custom Permissions changed the game when it came to creating programmatic checks for feature management within Salesforce. Between Custom Metadata and Custom Permissions, Salesforce as a whole has been trying to gently move people away from permissions management by way of hierarchical custom settings (or, even worse, iterating through Permission Sets!). And there's a lot to love when it comes to Custom Permissions. Since Winter '18, the FeatureManagement.checkPermission method has enabled developers to easily implement permission-based code routing. However ... when it comes time to testing feature-flagged code, how can we easily ensure that our tests remain isolated without polluting our test domain (or, even worse, unnecessarily exposing private methods merely to test the innards of a class)? Join me on the journey toward testing Custom Permissions painlessly!


linkIntro: Feature-based Code Routes

Let's say we have a business requirement that asks for a task to be created based off of Opportunity owners when an API interaction from an external system identifies outreach as the next best step. This integration could be the result of Opportunity Stages being updated by a Sales person; it could be kicked off by an internal cron job; it could come from anywhere. The business would like to gradually roll this feature out to users without fully opting all of them in at once. This is the perfect use-case for Custom Permissions: we can feature flag the logic that creates the task, and opt users from Sales in as we please from a Permission Set with the Custom Permission included:

1link<!-- Is_API_Task_Creation_Enabled.customPermission-meta.xml -->

2link<?xml version="1.0" encoding="UTF-8"?>

3link<CustomPermission xmlns="http://soap.sforce.com/2006/04/metadata">

4link <description>Should an API integration trigger the creation of Tasks for Sales users?</description>

5link <label>Is API Task Creation Enabled</label>

6link</CustomPermission>

And the Permission Set:

1link<!-- Create_API_Task_For_Sales.permissionset-meta.xml -->

2link<?xml version="1.0" encoding="UTF-8"?>

3link<PermissionSet xmlns="http://soap.sforce.com/2006/04/metadata">

4link <customPermissions>

5link <enabled>true</enabled>

6link <name>Is_API_Task_Creation_Enabled</name>

7link </customPermissions>

8link <hasActivationRequired>false</hasActivationRequired>

9link <label>Create API Task For Sales</label>

10link <license>Salesforce</license>

11link</PermissionSet>

Some example code, based off the premise that an update to an Opportunity triggers this action. It could be done synchronously, through a trigger handler, or asynchronously, through a Queueable or Batch job. We'll start with the test for the happiest path:

1link@isTest

2linkprivate class OpportunityTaskHandlerTests {

3link

4link @isTest

5link static void it_should_create_tasks_for_eligible_sales_people() {

6link Opportunity opp = new Opportunity(OwnerId = UserInfo.getUserId());

7link

8link new OpportunityTaskHandler().createTasksForEligibleSalespeople(

9link new List<Opportunity>{

10link opp

11link }

12link );

13link

14link Task createdTask = [SELECT Id, OwnerId FROM Task];

15link System.assertEquals(opp.OwnerId, createdTask.OwnerId, 'Owner Id didn\'t match for task!');

16link }

17link}

We'll get started on that OpportunityTaskHandler object in a second; for now, of course, the test fails with the classic:

1linkSystem.QueryException: List has no rows for assignment to SObject

Perfect. You'll note that we are actually getting an additional safety feature right out of the box; because there is no LIMIT command on the SOQL query for createdTask, we're also safe-guarding against future regressions where multiple Tasks might be introduced. With the advent of the Winter '21 release, you'll also note that next week we will be able to take advantage of the Safe Navigation feature to perform the same query:

1linkId actualOwnerId = [SELECT Id, OwnerId FROM Task]?.OwnerId;

2linkSystem.assertEquals(opp.OwnerId, actualOwnerId, 'Owner Id didn\'t match for task!');

Of course, such syntax sugar only avails us in the event that we only need to assert for one thing, but I point it out here in the event that you haven't checked the release notes recently.

linkGetting Our First Custom Permissions Test To Pass

Right now we have a failing test, but we also have zero functionality and no Custom Permissions wired up yet. Let's fix that:

1linkpublic without sharing class OpportunityTaskHandler {

2link public static final String TASK_SUBJECT = 'You have 10 days to move this sale along!';

3link

4link public void createTasksForEligibleSalespeople(List<Opportunity> opps) {

5link // here we will assume the passed in Opps are pre-filtered

6link if(FeatureManagement.checkPermission('Is_API_Task_Creation_Enabled')) {

7link this.createTasks(opps);

8link }

9link }

10link

11link private void createTasks(List<Opportunity> opps) {

12link List<Task> tasksToInsert = new List<Task>();

13link for(Opportunity opp : opps) {

14link Task t = new Task(

15link ActivityDate = System.today().addDays(10),

16link OwnerId = opp.OwnerId,

17link Subject = TASK_SUBJECT,

18link WhatId = opp.AccountId,

19link WhoId = opp.ContactId

20link );

21link tasksToInsert.add(t);

22link }

23link insert tasksToInsert;

24link }

25link}

We'll use a public static String for the Task Subject to aid in testing, but you could just as easily use a Custom Label. The only other design decision to talk about is the routing -- the reference to the Custom Permission itself. In a more complicated ask, and a more sophisticated system, you might also choose to use some form of configuration or metadata to inject the name of the Custom Permission being used; instead of hard-coding Is_API_Task_Creation_Enabled, you'd have the ability to swap Custom Permission(s) dynamically. James Hou has several interesting POCs on how this might be accomplished -- while these feature-flag systems are not production ready, looking through the patterns in that repo might help you in your own search for best practices regarding customizations like this. But I digress -- back to it.

We've got our functionality -- let's get back to our test! One thing we can do is validate that the test has been setup correctly before touching anything else:

1link// in OpportunityTaskHandlerTests

2link

3link@isTest

4linkstatic void it_should_create_tasks_for_eligible_sales_people() {

5link System.assertEquals(false, FeatureManagement.checkPermission('Is_API_Task_Creation_Enabled'));

6link // ...

7link}

One of a few things I'm not thrilled with concerning the checkPermission method? It should throw an exception, in my opinion, if you pass in a Custom Permission name that doesn't exist. It doesn't do that. This is one of the other reasons I brought up the feature-flagging framework, above -- it's important that you isolate and minimize String-based parameters, both in your tests and in production-level code. It's too easy for misspellings to go unnoticed, especially if you aren't giving yourself the safety net that tests represent. Though it consumes an extra SOQL call, there is some wisdom to be gained in wrapping the checkPermission method to validate that the Custom Permission in question actually exists ... for the moment, we'll hold off on implementing something like that.

Anyway. The test is still failing. Let's address that. One possible way to do so -- and the method we'll employ first -- is to assign the Permission Set featuring the Custom Permission to our test user. There are ample pitfalls to this approach -- which we'll cover shortly -- but we're head's-down in the "red, green, refactor" TDD methodology at the moment, and the only thing that matters presently is getting that test to pass.

Permission Sets are metadata; they're retrievable in our tests without having to use the 'seeAllData` test attribute (and you shouldn't be using that attribute anyway). If you aren't familiar with how Users are assigned to Permission Sets within Apex, the process is refreshingly simple:

1link@isTest

2linkstatic void it_should_create_tasks_for_eligible_sales_people() {

3link System.assertEquals(false, FeatureManagement.checkPermission('Is_API_Task_Creation_Enabled'));

4link

5link PermissionSet ps = [SELECT Id FROM PermissionSet WHERE Name = 'Create_API_Task_For_Sales'];

6link PermissionSetAssignment psa = new PermissionSetAssignment(

7link AssigneeId = UserInfo.getUserId(),

8link PermissionSetId = ps.Id

9link );

10link //...

11link}

Yes! Writing Apex is fun and easy! With any luck, we'll be done with this requirement before lun--

1linkSystem.QueryException: List has no rows for assignment to SObject

Wait, what. Why is there still no Task being created? Is there some kind of async process surrounding permissions that is preventing the call to FeatureManagement.checkPermissions from returning true? Sure enough, debugging shows the value has not changed even after the Permission Set has been assigned. Well, that's OK -- we're veterans of async deception in Apex, which means we know wrapping this thing in Test.startTest / Test.stopTest should force all async actions -- including the presumed permissions updating -- to complete. I'm thinking maybe I'll have a caprese sandwi--

1linkList has no rows for assignment to SObject

Hmm. OK, that ... didn't work. I didn't expect that. What about if we wrap the calling code in System.runAs? Even though we're already running the test as ourself, maybe there's something about running the test in another context that will help:

1link@isTest

2linkstatic void it_should_create_tasks_for_eligible_sales_people() {

3link System.assertEquals(false, FeatureManagement.checkPermission('Is_API_Task_Creation_Enabled'));

4link

5link PermissionSet ps = [SELECT Id FROM PermissionSet WHERE Name = 'Create_API_Task_For_Sales'];

6link PermissionSetAssignment psa = new PermissionSetAssignment(

7link AssigneeId = UserInfo.getUserId(),

8link PermissionSetId = ps.Id

9link );

10link

11link // See the repo if you haven't seen

12link // these Id generators before

13link Opportunity opp = new Opportunity(

14link AccountId = TestingUtils.generateId(Account.SObjectType),

15link ContactId = TestingUtils.generateId(Contact.SObjectType),

16link OwnerId = UserInfo.getUserId()

17link );

18link

19link

20link System.runAs(new User(Id = UserInfo.getUserId())) {

21link new OpportunityTaskHandler().createTasksForEligibleSalespeople(

22link new List<Opportunity>{

23link opp

24link }

25link );

26link }

27link

28link // Added asserts for all the functionality

29link Task createdTask = [SELECT Id, ActivityDate, OwnerId, WhatId, WhoId FROM Task];

30link System.assertEquals(System.today().addDays(10), createdTask.ActivityDate, 'Activity Date didn\'t match for task!');

31link System.assertEquals(opp.OwnerId, createdTask.OwnerId, 'Owner Id didn\'t match for task!');

32link System.assertEquals(opp.AccountId, createdTask.WhatId, 'What Id didn\'t match for task!');

33link System.assertEquals(opp.ContactId, createdTask.WhoId, 'Who Id didn\'t match for task!');

34link}

I'll spare you the drama -- the test is still failing. I'm recreating this experience, step-by-painful-step, as it happened to me when I first went to work on a feature like this. When it was happening to me, in the moment, I'll admit -- I was tempted to give up. I already had a test passing that verified the objects in question (which were not Opportunities for the project I was working on, but the same concept applies) were being filtered correctly. I knew the project's code coverage was high enough that a few untested -- and admittedly simple -- lines were unlikely to arouse suspicion or red flags. But that's not the Joys Of Apex way. Indeed, the thought of giving up so galled me that I was driven to continue. Deeper into the mysterious SObjects known as "Setup Objects" we will have to go ...

linkWorking With Setup Objects In Apex Tests

There are quite a few objects that belong to the "Setup Object" category, which becomes relevant to us since we would like to both manipulate these objects and perform other DML (the Task insertion) within our test. We are typically spoiled when it comes to documentation on the SFDC platform, and this is no exception. Here are some of the more pertinent objects that I've run into which can generate the dreaded "mixed DML" setup object error when writing unit tests:

It's that last one -- SetupEntityAccess -- which will prove crucial to aiding and abetting our unit tests. It turns out that in addition to the PermissionSetAssignment object, which is still required, we also need to ensure that our Permission Set is correctly set up with the reference for the Custom Permission in order for our test to work. This also forces us to make our test fully independent from the Permission Set that we've created -- which is great. Since Permission Sets can be changed without running all tests, it's possible to remove the Custom Permission we've created from our Permission Set and deploy without anybody being the wiser -- until our unit tests are run the next time a code update is deployed! We'll remove that possible failure point from our codebase and enjoy clean code in the process.

I'll also mention that even after all of this was pieced together, I still had to do the Test.startTest() / Test.stopTest() song and dance prior to finally having success with just the plain System.runAs(user) method -- only in the runAs context is a User's Custom Permission status successfully re-calculated during testing!

Here's what creating the full list of objects necessary to tie everything together looks like:

1link// in TestingUtils

2linkpublic static void activateCustomPerm(Id userId, String permissionName) {

3link PermissionSet ps = new PermissionSet(

4link Name = 'CustomPermissionEnabled',

5link Label = 'Custom Permisison Enabled'

6link );

7link insert ps;

8link

9link SetupEntityAccess sea = new SetupEntityAccess(

10link ParentId = ps.Id,

11link SetupEntityId = [

12link SELECT Id

13link FROM CustomPermission

14link WHERE DeveloperName = :permissionName

15link LIMIT 1

16link ].Id

17link );

18link

19link PermissionSetAssignment psa = new PermissionSetAssignment(

20link AssigneeId = userId,

21link PermissionSetId = ps.Id

22link );

23link

24link insert new List<SObject>{ sea, psa };

25link}

Putting it all together, our test now looks like:

1link@isTest

2linkstatic void it_should_create_tasks_for_eligible_sales_people() {

3link TestingUtils.activateCustomPerm(

4link UserInfo.getUserId(),

5link 'Is_API_Task_Creation_Enabled'

6link );

7link

8link Opportunity opp = new Opportunity(

9link AccountId = TestingUtils.generateId(Account.SObjectType),

10link ContactId = TestingUtils.generateId(Contact.SObjectType),

11link OwnerId = UserInfo.getUserId()

12link );

13link

14link // runAs is REQUIRED to recalc the user's permissions

15link System.runAs(new User(Id = UserInfo.getUserId())) {

16link new OpportunityTaskHandler().createTasksForEligibleSalespeople(

17link new List<Opportunity>{

18link opp

19link }

20link );

21link }

22link

23link Task createdTask = [SELECT Id, ActivityDate, OwnerId, WhatId, WhoId FROM Task];

24link // mobile friendly asserts

25link // sorry desktop users!

26link System.assertEquals(

27link System.today().addDays(10),

28link createdTask.ActivityDate,

29link 'Activity Date didn\'t match for task!'

30link );

31link System.assertEquals(

32link opp.OwnerId,

33link createdTask.OwnerId,

34link 'Owner Id didn\'t match for task!'

35link );

36link System.assertEquals(

37link opp.AccountId,

38link createdTask.WhatId,

39link 'What Id didn\'t match for task!'

40link );

41link System.assertEquals(

42link opp.ContactId,

43link createdTask.WhoId,

44link 'Who Id didn\'t match for task!'

45link );

46link}

Now we get a different error:

1linkINVALID_CROSS_REFERENCE_KEY, invalid cross reference id

This is because the Ids generated by TestingUtils aren't recognized by the database as valid -- because the given Account and Contact records do not exist. This is where dependency injection / use of the Stub API comes into play, going back to the Mocking DML article:

1link// in OpportunityTaskHandler

2linkprivate final ICrud crud;

3link

4linkpublic OpportunityTaskHandler(ICrud crud) {

5link this.crud = crud;

6link}

7link

8link private void createTasks(List<Opportunity> opps) {

9link List<Task> tasksToInsert = new List<Task>();

10link // loop through opps

11link // create tasks

12link this.crud.doInsert(tasksToInsert);

13link }

And in the test:

1linkSystem.runAs(new User(Id = UserInfo.getUserId())) {

2link new OpportunityTaskHandler(CrudMock.getMock()).createTasksForEligibleSalespeople(

3link new List<Opportunity>{

4link opp

5link }

6link );

7link}

8link

9linkTask createdTask = (Task)CrudMock.Inserted.Tasks.singleOrDefault;

Note that the singleOrDefault method throws if more than one element is present -- the same as our old SOQL query safe-guard. Excellent. And the test passes! But maybe you're more into the Stub API these days? This is a great chance to plug Suraj Pillai's UniversalMock Stub API framework for easy stubbing. There is one critical limitation with the Stub API, however -- you can't mock private methods.

This means that for mocking DML, you're still "stuck" having a DML wrapper of sorts -- which would then allow you to use the mock like so:

1link// In OpportunityTaskHandlerTests

2linkUniversalMocker mock = UniversalMocker.mock(Crud.class);

3linkICrud crudMock = (ICrud)mock.createStub();

4link

5linkSystem.runAs(new User(Id = UserInfo.getUserId())) {

6link new OpportunityTaskHandler(crudMock).createTasksForEligibleSalespeople(

7link new List<Opportunity>{

8link opp

9link }

10link );

11link}

12link

13linkList<Task> createdTasks = (List<Task>)mock.forMethod('doInsert').getValueOf('records');

14linkTask createdTask = createdTasks[0];

15link// etc with your asserts ...

For mocking DML, I find the use of the Stub API a bit heavy, but it's good to point out its flexibility to people who may not be aware that a whole host of other options are available to them when testing complicated objects.

linkWrapping Up Testing Custom Permissions

We've successfully decoupled our tests from any one Permission Set existing, and also shown how to test for the existence of Custom Permissions in isolation. The negative test -- simply verifying that no Task is created if the User does not have the Custom Permission enabled -- is left as a trivial exercise for the reader. The more serious task would be wrapping calls to FeatureManagement, as mentioned earlier, to validate that the Custom Permission exists -- you can afford the extra SOQL call, hopefully, but this also makes the method non-bulk-safe.

Anyway, we've planted the seed for extensible permissions-based routing. I don't have the answer for how to best dynamically gate functionality ... at the moment, I would probably go the route of Custom Metadata being fed into a system calling FeatureManagement.checkPermission, with sensible defaults. One problem with the dynamic version of feature flagging is that it puts the onus on the business as a whole to eliminate dead code routes when certain features are deprecated; if your tests are properly self-isolating, the only way you would know that code was no longer reachable would be if somebody went and deleted the Custom Permission in question ... otherwise, if it hangs out, without being assigned to any Permission Set, you have no intuitive, in-system, way to validate a feature being deprecated.

Despite this dead-code issue, I hope that I've given you plenty to think about when it comes to Custom Permissions. Worst case scenario, I'm simply confirming what you already know -- Custom Permissions play nicely within Apex; you just need to be sure your tests are properly decoupled. I've uploaded the example code if you want to browse through on Github -- till next time!

Note - the original version of this article can be read on jamessimone.net

Intro: Feature-based Code RoutesGetting Our First Custom Permissions Test To PassWorking With Setup Objects In Apex TestsWrapping Up Testing Custom Permissions

Home Apex Logging Service Apex Object-Oriented Basics Batchable And Queueable Apex Building A Better Singleton Continuous Integration With SFDX Dependency Injection & Factory Pattern Enum Apex Class Gotchas Extendable Apis Future Methods, Callouts & Callbacks Idiomatic Salesforce Apex Introduction & Testing Philosophy Lazy Iterators Lightweight Trigger Handler LWC Composable Modal LWC Composable Pagination LWC Custom Lead Path Mocking DML React Versus Lightning Web Components Refactoring Tips & Tricks Repository Pattern setTimeout & Implementing Delays Sorting And Performance In Apex Test Driven Development Example Testing Custom Permissions Writing Performant Apex Tests



Read more tech articles