API Reference: SPersona

Apex Stem Docs
Apex StemApexBlueprintSPersonarunAsAPI Reference
A builder for the restricted test users you run System.runAs against. Profile and PermissionSet assignment, safe defaults, and how it pairs with owner() / sharedWith() to declare hostile data.

API Reference: SPersona

SPersona is a builder for creating the restricted users ("personas") you run System.runAs against. Like SBlueprint, it is immutable — every method returns a new instance.

Writing permission tests means creating a test user, and that comes with a pile of subtle traps (a unique Username, mixed DML, locale defaults, resolving the Profile name). SPersona centralises the one correct implementation in a single class. Don't hand-write a UserFactory.

Static Factory

MethodPurpose
SPersona.of(String name)The entry point. Pass a short persona name ('sales-rep'), used for LastName and friends

Building

MethodPurpose
profile(String profileName)Set the Profile by name ('Standard User', 'Minimum Access - Salesforce'). Resolved at create() and cached for the whole test run
permissionSets(String permissionSetName)Add one PermissionSet by API name (not the label)
permissionSets(List<String> permissionSetNames)Same, several at once
set(String fieldName, Object value)Override any User field. Same last-wins semantics as SBlueprint.set()

Creating

MethodReturnsPurpose
create()UserResolves the profile, builds the User with safe defaults, inserts it together with its PermissionSetAssignments, and returns it
APEX
User rep = SPersona.of('sales-rep')
  .profile('Standard User')
  .permissionSets('InvoiceReadOnly')
  .set('LanguageLocaleKey', 'en_US')
  .create();

Behaviour worth knowing

  • Usernames are issued as UUIDs, so they never collide under parallel test execution
  • Locale defaults come from the running user, keeping them org-independent. Override with set() if you need to
  • Mixed DML is avoided internally (the insert is wrapped in System.runAs), so you can mix it freely with ordinary DML
  • create() is test-context only. It relies on System.runAs, so production code cannot call it
  • Nothing is cached per call. Every call creates a fresh user (only the Profile-name-to-Id resolution is cached)
  • ⚠️ Profile names are locale-dependent. 'Standard User' in an English org, '標準ユーザー' in a Japanese one

Use it with hostile data

SPersona earns its keep alongside SBlueprint's owner() / sharedWith(). Together they declare hostile data: owned by an admin, with only the bare minimum shared to the persona under test.

APEX
User admin = SPersona.of('admin').profile('System Administrator').create();
User rep   = SPersona.of('sales-rep').profile('Standard User').create();
 
SOrchestrator orchestrator = SOrchestrator.start()
  .add(
    SBlueprint.of(Invoice__c.class).alias('inv')
      .owner(admin)                //  owned by admin
      .sharedWith(rep, 'Read'));   //  rep gets Read only
orchestrator.create();
 
System.runAs(rep) {
  // Verify what rep can see, and what rep can write
}

Whether a record was sharedWith becomes the expected visibility. If a record you never shared turns up inside the runAs block, that is a hole in your sharing configuration.

Consolidating on the project side

Follow the same idea as Blueprints.cls: keep project-specific personas in a single Personas.cls. Spell Profile names and PermissionSet names inline across tests and a rename on the org side scatters the fix across every file.

APEX
public with sharing class Personas {
  /** A rank-and-file sales rep */
  public static User salesRep() {
    return SPersona.of('sales-rep')
      .profile('Standard User')
      .permissionSets('InvoiceReadOnly')
      .create();
  }
}