Encountering a System.DmlException: MIXED_DML_OPERATION is a strict blocker during Salesforce deployments. This error occurs when a developer attempts to perform Data Manipulation Language (DML) operations on both Setup objects and Non-Setup objects within the exact same transaction.
In a testing environment, this typically manifests when provisioning a test user (Setup object) and immediately creating test records like Accounts or Opportunities (Non-Setup objects) assigned to that user. Resolving this requires explicit manipulation of transaction contexts.
The Root Cause of Mixed DML Operation Exceptions
To understand the fix, you must understand the platform's architecture. Salesforce categorizes database entities into two primary groups:
- Setup Objects: Objects that affect system security, access controls, and organization structure (e.g.,
User,UserRole,Profile,PermissionSet,Group). - Non-Setup Objects: Standard and custom objects that hold business data (e.g.,
Account,Contact,CustomObject__c).
When a Setup object is modified, Salesforce immediately queues asynchronous recalculations for sharing rules, role hierarchies, and record visibility. If you modify an Account in the same transaction as a User, the platform cannot guarantee the integrity of the data visibility model for that Account. The database prevents this race condition by throwing a Mixed DML Operation Exception.
The Fix: Isolating Contexts in Salesforce Apex Test Setup
To bypass this limitation in test classes, you must force Salesforce to evaluate the DML operations in separate contexts. In Salesforce Apex unit testing, the most efficient and native way to achieve this is by utilizing the System.runAs() method.
The System.runAs() block instructs the Apex runtime to temporarily suspend the current transaction boundaries, execute the enclosed code in a distinct DML context, and then resume.
Implementation: The Context Separation Pattern
The standard architectural pattern is to insert your Setup objects in the main execution context, and wrap your Non-Setup object DML inside a System.runAs() block using the context of the executing user.
@isTest
private class UserProvisioningTest {
@testSetup
static void setupTestData() {
// 1. Perform DML on Setup Objects
Profile standardProfile = [SELECT Id FROM Profile WHERE Name = 'Standard User' LIMIT 1];
User testUser = new User(
Alias = 'tuser',
Email = 'mixeddml.test@example.com',
EmailEncodingKey = 'UTF-8',
LastName = 'Testing',
LanguageLocaleKey = 'en_US',
LocaleSidKey = 'en_US',
ProfileId = standardProfile.Id,
TimeZoneSidKey = 'America/Los_Angeles',
UserName = 'mixeddml.test.' + DateTime.now().getTime() + '@example.com'
);
// This is a Setup Object DML
insert testUser;
// 2. Isolate Non-Setup Object DML
// Instantiate the running user to establish a new transaction context
User contextAdminUser = new User(Id = UserInfo.getUserId());
System.runAs(contextAdminUser) {
// This block operates in a segregated DML context
Account testAccount = new Account(
Name = 'Test Account for Provisioned User',
OwnerId = testUser.Id
);
// This Non-Setup Object DML will no longer throw the Mixed DML exception
insert testAccount;
}
}
@isTest
static void testAccountOwnership() {
Account acc = [SELECT Id, Owner.Email FROM Account LIMIT 1];
System.assertEquals('mixeddml.test@example.com', acc.Owner.Email, 'Account owner email should match the provisioned user.');
}
}
Deep Dive: Why System.runAs Setup Objects Work
Using System.runAs setup objects circumvents the transaction lock. When the Apex engine encounters the runAs statement, it commits the pending Setup DML operations to the local test database context. It then initializes a simulated user session.
Because the Setup DML is technically evaluated before the runAs block executes, the sharing calculations are finalized for the scope of the test. When the Non-Setup DML (the Account insertion) fires inside the block, it acts as an entirely isolated database transaction from the perspective of the platform's security engine.
This technique is specifically engineered for test classes. It ensures that your Salesforce Apex test setup remains robust and highly cohesive, keeping data generation centralized without needing complex asynchronous test patterns.
Common Pitfalls and Edge Cases
Production Code Limitations
A critical distinction for developers: System.runAs() is exclusively available in test classes. If you encounter a Mixed DML Operation Exception in production (e.g., an Apex Trigger or standard class), you cannot use this fix. Production code requires offloading the Setup DML to an asynchronous process using @future methods or Queueable Apex.
Over-nesting runAs Blocks
Avoid nesting multiple System.runAs() statements. While valid syntactically, it obscures test logic and can lead to unexpected governor limit consumption. Structure your test data factories to perform all Setup DML upfront, followed by a single runAs block for all business data.
Role Hierarchy Complexity
If your test requires creating a UserRole and a User, both are Setup objects, but they also have strict dependency rules. You must insert the UserRole first, assign it to the User, insert the User, and then use System.runAs to insert records.
// Correct execution order for complex setup structures
UserRole newRole = new UserRole(DeveloperName = 'TestRole', Name = 'Test Role');
insert newRole; // Setup DML 1
User testUser = new User(... UserRoleId = newRole.Id);
insert testUser; // Setup DML 2
System.runAs(new User(Id = UserInfo.getUserId())) {
insert new Account(Name = 'Test Data'); // Non-Setup DML
}
Conclusion
The System.DmlException: MIXED_DML_OPERATION is a protective mechanism enforcing the integrity of Salesforce's sharing model. By utilizing System.runAs() to enforce strict transaction boundaries within your test classes, you satisfy the platform's security requirements while maintaining deterministic, tightly coupled test data setups. Centralize this pattern in your Test Data Factory classes to eliminate this deployment blocker across your entire codebase.