Skip to main content
New tool CRON Expression Builder — preview next run times before you schedule Apex. Open the builder →
Salesforce logo with integrated circuit patterns, illustrating Apex test user mode.
Apex

Apex Test User Mode: System.runAs for Permissions

Since API 67.0, Apex tests run in user mode by default, so your tests have to account for user permissions explicitly. System.runAs is how you isolate and validate permission sets across a test suite.

The short answer

From API version 67.0 (Summer '26), Apex tests run in user mode by default, so they execute with the permissions of whoever runs them. System.runAs is how you impersonate a specific user, wrap both the data setup and the assertion, and prove a permission set grants what it should and denies what it should not.

Key takeaways Apex tests now run in user mode by default, so the permissions of the user you test with matter. System.runAs() is the primary mechanism for impersonating users and testing Permission Sets in Apex. Settle on distinct user personas and their permissions before you implement security in Apex or write the tests. Write positive and negative security tests: one proves users have the access they should, the other proves they are denied what they should not. Add system-level security assertions so sensitive operations stay out of reach of general users.

Understanding user mode in Apex tests

Starting with API version 67.0 (Summer '26 release), Apex tests default to running in user mode. Your tests now execute with the permissions and access rights of the user running the test. That is a good move for security, and it can also break tests that never accounted for a specific user context.

The shift changes how you have to test Apex code and the Permission Sets that go with it. System.runAs() is the method that gives you proper isolation and lets you check the security behavior of your code.

The role of System.runAs()

By default, tests run as the executing user, inheriting their object, field, and record access. Without isolation, a security flaw hides behind whatever access that user happens to have. System.runAs() lets you impersonate a specific user, so your test execution gets their defined permissions, and that is how you test the way your code behaves under different security profiles.

Defining security roles

Work out what security looks like from an administrative perspective before you write any enforcement code or testing strategy. Identify the user personas or roles responsible for managing specific data sets: who creates, who edits, who only consumes the information.

A quote calculation engine, for instance, might need a "Pricing Admin" persona with rights to manage pricing and discount rules, and a "Sales User" persona who consumes that data to generate quotes but cannot modify the rules themselves. Your Apex code enforces the distinction. Your tests should prove it with System.runAs().

Implementing System.runAs() in tests

Create the test users inside your test setup, assign each one the Permission Set it needs, then wrap both the data creation and the test execution in System.runAs().

In pseudocode:

@testSetup
static void setup() {
    // Create test users with specific permission sets
    createStandardUserWithPermissionSet('QuoteApp_PricingAdmin', PRICING_ADMIN_LASTNAME, PRICING_ADMIN_EMAIL);
    createStandardUserWithPermissionSet('QuoteApp_SalesUser', SALES_USER_LASTNAME, SALES_USER_EMAIL);

    // Execute as Pricing Admin to set up core pricing data
    System.runAs(getTestPricingAdminUser()) {
        insert new PriceConfig__c(/* required fields */);
        insert new PriceRule__c(/* required fields */);
    }

    // Execute as Sales User to set up transactional records
    System.runAs(getTestSalesUser()) {
        insert new Quote__c(OwnerId = salesUser.Id, /* ... */);
        insert new QuoteLine__c(/* ... */);
    }
}

@IsTest
static void addQuoteLine_asSalesUser() {
    System.runAs(getTestSalesUser()) {
        // Given: Query Quote created by Sales User
        Quote__c quote = [SELECT Id FROM Quote__c WHERE OwnerId = :salesUser.Id LIMIT 1];

        // When: Calculate Quote
        Test.startTest();
        QuoteService.addLine(quote.Id, /* product, qty, etc. */);
        Test.stopTest();

        // Then: Query Quote line calculated
        QuoteLine__c line = [SELECT Id, UnitPrice__c FROM QuoteLine__c WHERE Quote__c = :quote.Id LIMIT 1];
        System.assertNotEquals(null, line.UnitPrice__c, 'Line should be priced using readable config/rules');
    }
}

@IsTest
static void createPriceRule_asPricingAdmin() {
    System.runAs(getTestPricingAdminUser()) {
        // When: Creating a pricing rule
        Test.startTest();
        Id ruleId = PricingAdminService.createPriceRule(new PriceRule__c(/* ... */));
        Test.stopTest();

        // Then: Pricing rule successfully created
        System.assertNotEquals(null, ruleId);
    }
}

Negative security testing

Negative tests are what prove a Permission Set is actually restricted. Check that a user holding it cannot perform actions outside its scope.

This test verifies that the "Sales User" permission set does not grant access to create pricing rules:

@IsTest
static void createPriceRule_deniedForSalesUser() {
    User salesUser = getTestSalesUser();
    // Given: Sales user
    System.runAs(salesUser) {
        Test.startTest();
        try {
            // When: Attempting to create pricing rule
            PricingAdminService.createPriceRule(new PriceRule__c(/* ... */));
            System.assert(false, 'Expected a security or access failure');
        } catch (Exception e) {
            // Then: Security enforced / also see DMLException methods
            System.assert(
                e.getMessage().toLowerCase().contains('insufficient') ||
                e.getMessage().toLowerCase().contains('access'),
                'Unexpected: ' + e.getMessage()
            );
        }
        Test.stopTest();
    }
}

Broader security assertions

It is worth writing tests that assert general users lack access to objects or fields that should only be accessible in system mode. Confirm, for example, that neither permission set allows write operations to a logging object.

Originally reported by andyinthecloud.com

Newsletter

One email every Tuesday

New guides, tool updates, and the release-note changes that break things.

No spam. Unsubscribe in one click.

Comments

Loading comments...

Leave a Comment