I noticed that the Running User context of a Queueable Job in a unit test depends on who calls the Test.stopTest()
method. While when code is ran normally (trigger, anonymous, …) the User context of the Queueable is the same as the user who put it on the queue.
So if you use System.runAs
in a unit test to queue a job, the user context of the job will be different if you call Test.stopTest()
inside or outside of the System.runAs
block. Basically you can produce a scenario that would never arise normally. If you don’t call Test.stopTest()
at all, the job is executed at the end of the unit test, again in context of the running user of the test and not the user who queued the job.
It cost me some time debugging a failing test that used the UserInfo.getUserId()
in a trigger with some dependent logic so I created a simple Queueable class to help me investigate. I might have just missed some piece of documentation, but hopefully this can save someone else some time.
public with sharing class QueueableClass implements Queueable {
public void execute(QueueableContext context) {
System.debug(LoggingLevel.DEBUG, 'InQueueable: ' + UserInfo.getName());
}
}
I ran this anonymous script to confirm the normal scenario
System.debug(LoggingLevel.DEBUG, 'InAnonymousAs: ' + UserInfo.getName());
System.enqueueJob(new QueueableClass());
Then I wrote some unit tests and noted who the running user is inside the Queueable job:
@IsTest
private class QueueableTest {
@IsTest
static void queueableRanAsRunningUser() {
//DEBUG|InQueueable: User User
User newUser = getTestUser();
System.debug(LoggingLevel.DEBUG, 'InTest: ' + UserInfo.getName());
Test.startTest();
System.runAs(newUser) {
System.debug(LoggingLevel.DEBUG, 'InRunAs: ' + UserInfo.getName());
System.enqueueJob(new QueueableClass());
}
Test.stopTest();
}
@IsTest
static void queueableRanAsTestUser() {
//DEBUG|InQueueable: TestUser
User newUser = getTestUser();
System.debug(LoggingLevel.DEBUG, 'InTest: ' + UserInfo.getName());
System.runAs(newUser) {
Test.startTest();
System.debug(LoggingLevel.DEBUG, 'InRunAs: ' + UserInfo.getName());
System.enqueueJob(new QueueableClass());
Test.stopTest();
}
}
@IsTest
static void queueableRanAsRunningUserNoStopTest() {
//DEBUG|InQueueable: User User
User newUser = getTestUser();
System.debug(LoggingLevel.DEBUG, 'InTest: ' + UserInfo.getName());
System.runAs(newUser) {
System.debug(LoggingLevel.DEBUG, 'InRunAs: ' + UserInfo.getName());
System.enqueueJob(new QueueableClass());
}
}
private static User getTestUser() {
Id profileIdToUse = UserInfo.getProfileId();
String rnd = String.valueOf((Math.random())).left(5);
return new User(
ProfileId = profileIdToUse,
LastName = 'TestUser',
Username = 'testuser' + rnd + '@invalid.test',
Email = 'testuser@invalid.test',
EmailEncodingKey = 'UTF-8',
LanguageLocaleKey = 'en_US',
TimeZoneSidKey = 'GMT',
LocaleSidKey = 'en_US',
Alias = 'tst',
IsActive = true
);
}
}
As pointed out by Adrian Larson in my Stack Exchange question this is probably as designed. However, I still think it’s not intuitive and does not follow what productive code would do.
Other Traps
LWC in Purgatory
If you are delivering your SFDX projects using packaging you are likely aware of the challenges with moving metadata artefacts from one package to another. Particularly when moving up the dependency hierarchy. There is this very neat “trick” for Unlocked Packages where you can temporarily remove metadata from a package…