Skip to content

Keep the Proof Running

Module 3 of 6 in the chapter Testing the Security Model, chapter 13 of Securing Salesforce: Sharing and Visibility. Each module stands on its own.

The question this module answers: Module 1’s three users prove a model today. What proves it again after every deployment?

The tests in modules 1 and 2 are run by people. They find today’s mistakes. But a sharing model isn’t finished on go-live day. Someone changes a trigger, edits a flow, adds a permission set, and nobody logs in as the user who shouldn’t see the record. The tests that run every time code is deployed are Apex tests. And the way an Apex test becomes a user is run as.

In class I asked the room: we know we can do a run as. What’s the one reason? Where do we put it? You can only do it in a unit test. And the purpose is to run as that user. If you have no data, chances are you’ll even have to create the user. So: create the user, run as the user, and then run your unit test as that user.

Salesforce’s words: “With the System method runAs, you can write test methods that change the user context to an existing user or a new user. Then that user’s sharing rules and object-level and field-level permissions are enforced” (Using the runAs Method (opens in a new tab), Apex Developer Guide, Winter ‘27, checked 2026-11-07).

Chapter 4 used it for one field. Here it’s the three users, for a whole sharing design.

From the same page:

  • It wins over the test class. “The user’s sharing rules and object-level and field-level permissions are enforced within a runAs block, regardless of the sharing mode (with sharing or without sharing) of the test class.”
  • But not over the code you call. “If a user-defined method is called in the runAs block, the sharing mode enforced is that of the class where the method is defined, not the test class.” So if your class is without sharing, run as doesn’t make it respect sharing. Your test proves what the class does, as written.
  • Tests only. “You can use runAs only in test methods.”
  • Licenses don’t matter. “The runAs method ignores user license limits. You can create users with runAs even if your organization has no additional user licenses.” So you can create one test user per persona.
  • It counts. “Every call to runAs counts against the total number of DML statements issued in the process.”
  • It solves mixed DML in tests. “You can also use the runAs method to perform mixed DML operations in your test by enclosing the DML operations within the runAs block.” That’s how a test creates users and records in the same test method.

A test starts with nothing. “By default, Apex test methods (API version 24.0 and later) can’t access pre-existing org data such as standard objects, custom objects, and custom settings data. They can only access data that they create.” The exception is setup: “objects that are used to manage your organization or metadata objects can still be accessed in your tests” (Understanding Test Data (opens in a new tab), Apex Developer Guide, Winter ‘27, checked 2026-11-07). And it cleans up after itself: “Apex test data is transient and isn’t committed to the database.”

So the test can find your profiles, permission sets and custom metadata, but it creates its own users and records. Build each test user like module 2’s persona: a profile, the persona’s permission sets as permission set assignments, and the fields your sharing rules read. If the persona uses a permission set group, the test has to calculate it first: Test.calculatePermissionSetGroup “Calculates aggregate permissions in specified permission set groups for testing” (Test Class (opens in a new tab), Apex Reference Guide, checked 2026-11-07).

Here’s the apple design from chapter 11 module 5, under test. Apples are Private. The rules say the Orchard department gets the green apples, and the trigger writes those shares with the Party reason. Three users: an owner, an Orchard user who should see a green apple, and a Bakery user who shouldn’t.

This is a sketch, like chapter 11’s code: it assumes that design, and a permission set called Apple User that opens the object.

@IsTest
private class AppleSharingTest {
static User makeUser(String alias, String department) {
Profile p = [SELECT Id FROM Profile WHERE Name = 'Standard User'];
return new User(
Alias = alias, LastName = alias, Department = department,
Email = alias + '@example.com',
UserName = alias + DateTime.now().getTime() + '@example.com',
ProfileId = p.Id, EmailEncodingKey = 'UTF-8',
LanguageLocaleKey = 'en_US', LocaleSidKey = 'en_US',
TimeZoneSidKey = 'America/Los_Angeles');
}
@IsTest
static void orchardSeesGreenApplesBakeryDoesNot() {
User owner = makeUser('owner', 'Office');
User orchard = makeUser('orchard', 'Orchard');
User bakery = makeUser('bakery', 'Bakery');
List<User> users = new List<User>{ owner, orchard, bakery };
PermissionSet appleUser = [SELECT Id FROM PermissionSet WHERE Name = 'Apple_User'];
// Users and their permission sets are setup objects:
// create them in their own runAs block, and let the user trigger's jobs finish.
Test.startTest();
System.runAs(new User(Id = UserInfo.getUserId())) {
insert users;
List<PermissionSetAssignment> grants = new List<PermissionSetAssignment>();
for (User u : users) {
grants.add(new PermissionSetAssignment(
AssigneeId = u.Id, PermissionSetId = appleUser.Id));
}
insert grants;
}
Test.stopTest();
// The owner creates a green apple; the trigger shares it with the rules.
Apple__c apple = new Apple__c(Name = 'Granny Smith', Color__c = 'Green');
System.runAs(owner) {
insert apple;
}
// The positive: the Orchard user sees it.
System.runAs(orchard) {
Assert.areEqual(1,
[SELECT COUNT() FROM Apple__c WHERE Id = :apple.Id WITH USER_MODE],
'Orchard should see the green apple');
}
// The negative: the Bakery user doesn't.
System.runAs(bakery) {
Assert.areEqual(0,
[SELECT COUNT() FROM Apple__c WHERE Id = :apple.Id WITH USER_MODE],
'Bakery should not see the green apple');
}
}
}

Look at what it checks. Not “did the trigger run,” and not “is there a row.” It checks what each user can see, as that user. That’s module 1’s habit, written down so it runs on every deployment.

And the user’s door needs its own test. Chapter 11 said sharing code has two doors: the record and the user. So write a second test: move the Bakery user to the Orchard department inside Test.startTest() and Test.stopTest(), so the user trigger’s queueable job runs, and then run as that user and expect to see the apple. If someone deletes the user trigger next year, that test fails before the release does.

What a run-as test proves, and what it doesn't What a run-as test proves, and what it doesn't. It proves: what one user can see and do in code, with their sharing and their object and field permissions; that both doors of the share logic still work; again on every deployment. It doesn't prove: the class's own sharing mode, which is kept for methods it calls; pages, list views, search and login flows, which a test never opens; data it didn't create, because tests start empty. Footnote: run as keeps the proof running; module 1's three users still check what a person sees on the screen. {"author": "Steve Simpson", "blueprint": "1.0.0", "builder": "standard 1.58.0", "built": "2026-11-04", "id": "DGM-SVA-TA-03", "rights": "\u00a9 2026 Twinshark LLC. All rights reserved.", "template": "comparison 0.5.0", "theme": "stevetecharc 1.4.0"} It proves in code, every deployment What one user can see and do Their sharing, object and field permissions Both doors of the share logic Again on every deployment It doesn't prove what a test never opens The code's own sharing mode, kept for methods it calls Pages, list views, search and login flows Data it didn't create: tests start empty Run as keeps the proof running. Module 1's three users still check what a person sees on the screen. © 2026 Twinshark LLC · stevetecharc.com
Click to enlarge

Run as is a test of code, in a test. Know its edges:

  • No screens. A test never opens a page, a list view or global search. The bleed checks from module 1 are still a person’s job.
  • No front door. It doesn’t log in, so login flows and session settings aren’t part of it.
  • Only its own data. It sees what it creates, plus setup and metadata. A problem that only shows up with production volumes or old records won’t show up here.
  • The code’s own mode still counts. A without sharing class you call stays without sharing. That’s not a gap in run as; it’s what you’re testing.

So run as doesn’t replace module 1. It keeps module 1’s answers true after the people who tested have moved on.

Test knowledge. Know that System.runAs can only be used in test methods, that it enforces the user’s sharing and object and field permissions inside the block, whatever the test class’s sharing mode, and that it ignores license limits so tests can create users. Know that a method called inside the block runs with its own class’s sharing mode. Know that runAs also lets a test do mixed DML. A likely area, not a known question.

  • That every class or flow that writes or reads shares has run-as tests for the user who should see and the user who shouldn’t.
  • That both doors are tested: the record and the user.
  • The test users each test builds, matching module 2’s personas: profile, permission sets, groups and the fields the rules read.
  • That tests assert what users see, not only that code ran or rows exist.
  • Which checks stay manual because tests can’t see them: pages, list views, search and logins.

Beyond the exam: what AI changes. When an AI assistant writes Apex for you, it will often write a test that runs as the admin and checks that code ran. Ask it for the run-as tests from this module: one as the user who should see, one as the user who shouldn’t, and one for the user’s door. And when an agent action is Apex, the same tests apply to the agent’s user. Chapter 20 covers agents.