---
name: apex-system-runas
description: "System.runAs in Apex tests: user-context impersonation, mixed-DML workaround, profile/permission testing, sharing verification, FLS NOT enforced, runAs nesting limits. NOT for general test setup (use apex-test-setup-patterns). NOT for WITH USER_MODE SOQL (use apex-user-mode-patterns)."
category: apex
salesforce-version: "Spring '25+"
well-architected-pillars:
  - Security
  - Reliability
tags:
  - apex
  - testing
  - system-runas
  - sharing
  - mixed-dml
triggers:
  - "system.runas apex test user context profile"
  - "runas mixed dml setup object user insert workaround"
  - "testing sharing rule apex runas community user"
  - "runas does not enforce field level security apex"
  - "runas version mode system context running user"
  - "nested system.runas apex test limit"
inputs:
  - Target user profile/permset
  - Permission/sharing scenario under test
  - Setup-DML requirements
outputs:
  - runAs-wrapped test block
  - User fixture creation
  - FLS caveat notes
dependencies: []
version: 1.0.0
author: Pranav Nagrecha
updated: 2026-04-22
---

# Apex System.runAs

Activate when writing Apex tests that need to exercise a specific user's perspective — profile, permission set, role, sharing context — or when unblocking mixed-DML errors in `@TestSetup`. `System.runAs` impersonates a target user for the scope of a block and flushes setup-object DML. Known caveat: FLS is NOT enforced inside `runAs`.

## Before Starting

- **Identify the user context.** Profile, permission set, role, ownership context — each may matter.
- **Confirm you need impersonation.** If testing code that uses `WITH USER_MODE` or security enforcement, runAs alone may not cover FLS.
- **Plan for mixed DML.** User/Group/UserRole DML cannot mix with sObject DML in one transaction — runAs is the escape hatch.

## Core Concepts

### Basic runAs

```
User u = [SELECT Id FROM User WHERE Profile.Name = 'Standard User' LIMIT 1];
System.runAs(u) {
    // UserInfo.getUserId() returns u.Id inside this block
    insert new Account(Name = 'Owned by u');
    List<Account> visible = [SELECT Id FROM Account];
    // Sharing rules applied as user u
}
```

Applies to: `UserInfo.*`, sharing enforcement, profile CRUD, record ownership.

Does NOT apply to: FLS (fields silently accessible regardless of profile), System.runAs itself (admin can still run it).

### Mixed DML workaround

Setup objects (`User`, `UserRole`, `Group`, `GroupMember`, `PermissionSet*`, `Profile`) cannot be DML-ed in the same transaction as sObject DML. `runAs` partitions the operations:

```
System.runAs(new User(Id = UserInfo.getUserId())) {
    User u = new User(...);
    insert u;  // setup-object DML isolated
}
insert new Account(Name = 'X');  // non-setup DML — legal
```

The pattern `runAs(new User(Id = UserInfo.getUserId()))` is a no-op impersonation that exists only for the mixed-DML boundary.

### Nested runAs

Allowed up to 20 levels. Exits back to the previous user when the block ends. Rarely useful; flat is better.

### Version behavior

If the running user has `ModifyAllData` / `ViewAllData`, sharing is still bypassed inside `runAs` unless the target user lacks those perms. Make sure the target `User` record reflects the profile you mean to test.

### FLS is NOT enforced

```
System.runAs(lowPrivUser) {
    Account a = [SELECT Hidden__c FROM Account];
    String x = a.Hidden__c;  // NO FLS ERROR — even though lowPrivUser can't see Hidden__c
}
```

For FLS enforcement use `WITH USER_MODE` SOQL, `Security.stripInaccessible`, or `with sharing` in combination with explicit `Schema.DescribeFieldResult.isAccessible()` checks.

## Common Patterns

### Pattern: Sharing-rule verification test

```
@IsTest static void testOwnershipVisibility() {
    User salesUser = TestUserFactory.salesUser();
    insert salesUser;
    System.runAs(salesUser) {
        insert new Account(Name = 'Private');
    }
    User otherSalesUser = TestUserFactory.salesUser();
    insert otherSalesUser;
    System.runAs(otherSalesUser) {
        System.assertEquals(0, [SELECT COUNT() FROM Account WHERE Name = 'Private']);
    }
}
```

### Pattern: Permission-set assignment test

```
User u = TestUserFactory.user();
insert u;
PermissionSet ps = [SELECT Id FROM PermissionSet WHERE Name = 'MyFeature'];
insert new PermissionSetAssignment(AssigneeId = u.Id, PermissionSetId = ps.Id);
System.runAs(u) {
    // exercise code gated by the permset
}
```

### Pattern: Mixed-DML boundary in @TestSetup

```
@TestSetup
static void setup() {
    System.runAs(new User(Id = UserInfo.getUserId())) {
        User u = new User(...);
        insert u;
    }
    insert new Account(Name = 'A');  // legal
}
```

## Decision Guidance

| Goal | Tool |
|---|---|
| Test a different profile's visibility | `System.runAs(user)` |
| Unblock mixed-DML in setup | `System.runAs(new User(Id = UserInfo.getUserId()))` |
| Verify FLS enforcement | `WITH USER_MODE` or `Security.stripInaccessible` — NOT runAs |
| Test community/guest user | Create a Customer Community user → `runAs` |
| Verify record ownership effects | `runAs(ownerUser)` + DML + assertions |

## Recommended Workflow

1. Create/query the target `User` record; confirm profile + perm sets reflect reality.
2. Wrap the user-context code in `System.runAs(u) { ... }`.
3. For setup-DML mixing sObjects, wrap the User/Group insert in `runAs(new User(Id = UserInfo.getUserId()))`.
4. Assert on query results AS THE TARGET USER.
5. For FLS, add a separate test using `WITH USER_MODE`.
6. Document the caveat in the test comment: "runAs does not enforce FLS."
7. Avoid nesting runAs; refactor into separate methods.

## Review Checklist

- [ ] Target user's profile/permset reflects the scenario
- [ ] Mixed-DML uses `runAs(new User(Id = UserInfo.getUserId()))` guard
- [ ] FLS-sensitive code has a parallel `WITH USER_MODE` test (runAs alone insufficient)
- [ ] No nested runAs beyond 2 levels
- [ ] Community/guest tests use the correct Customer Community license

## Salesforce-Specific Gotchas

1. **FLS is silently bypassed inside runAs.** Tests pass while FLS-violating code ships to production.
2. **`runAs` only works in test context** — calling it in non-test code throws.
3. **`runAs(someUser)` inherits governor limits from the outer transaction** — does not give you a fresh limit budget.
4. **Querying a User with `WHERE Profile.Name = ...` may hit profile-name changes across orgs.** Prefer `UserType = 'Standard'` plus permset assignments.

## Output Artifacts

| Artifact | Description |
|---|---|
| `TestUserFactory` | Reusable user builders per profile |
| runAs-wrapped test methods | User-context sharing tests |
| FLS parallel test | `WITH USER_MODE` or `stripInaccessible` coverage |

## Related Skills

- `apex/apex-test-setup-patterns` — overall test structure
- `apex/apex-user-mode-patterns` — `WITH USER_MODE` / `with sharing`
- `security/crud-and-fls-enforcement` — FLS enforcement patterns
