API Reference: SPersona
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
| Method | Purpose |
|---|---|
SPersona.of(String name) | The entry point. Pass a short persona name ('sales-rep'), used for LastName and friends |
Building
| Method | Purpose |
|---|---|
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
| Method | Returns | Purpose |
|---|---|---|
create() | User | Resolves the profile, builds the User with safe defaults, inserts it together with its PermissionSetAssignments, and returns it |
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 onSystem.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.
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.
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();
}
}
Related Documents
- API Reference: SBlueprint: declaring hostile data with
owner/sharedWith - API Reference: SOrchestrator: dependency resolution and real DML
- ApexBlueprint guide: back to the guide index