Reputation: 10752
I would like my app to run special code (e.g. resetting its state) when running in UI Testing mode. I looked at environment variables that are set when the app is running from UI Testing and there aren't any obvious parameters to differentiate between the app running normally vs in UI Testing. Is there a way to find out?
Two workarounds that I'm not satisfied with are:
XCUIApplication.launchEnvironment
with some variable that I later check in the app. This isn't good because you have to set it in the setUp
method of each test file. I tried setting the environment variable from the scheme settings but that doesn't propagate to the app itself when running UI Testing tests.__XPC_DYLD_LIBRARY_PATH
. This seems very hacky and might only be working now because of a coincidence in how we have our target build settings set up.Upvotes: 59
Views: 22735
Reputation: 660
I'm surprised that this question has been open for 9 years without anyone mentioning Swizzling!
Your Swift app can detect if UI tests are running, without you to touching launchEnvironment
or launchArguments
in all of your UI test classes. All you need to do is set up a single file, which intercepts the XCUIApplication.launch()
method, modifies the environment, then resumes execution by calling the original launch method.
Don't fear adding a little Objective-C to your Swift app! All you need to do is add this file (and letting Xcode generate a bridging header, if necessary) to your UI Test target, and all of your UI tests in that target will launch with the UI_Test
environment variable set.
#import <XCTest/XCTest.h>
#import <objc/runtime.h>
@implementation XCUIApplication (Swizzle)
+ (void)load {
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
SEL originalSelector = @selector(launch);
SEL swizzledSelector = @selector(swizzledLaunch);
Method originalMethod = class_getInstanceMethod(self, originalSelector);
Method swizzledMethod = class_getInstanceMethod(self, swizzledSelector);
method_exchangeImplementations(originalMethod, swizzledMethod);
});
}
- (void)swizzledLaunch {
// Create a mutable copy of launchEnvironment
NSMutableDictionary *mutableEnv = [self.launchEnvironment mutableCopy];
// Set the environment variable on the mutable copy
mutableEnv[@"UI_Test"] = @"true";
// Reassign the modified environment to the app's launchEnvironment
self.launchEnvironment = mutableEnv;
// Call the original launch (which is now `swizzledLaunch`)
[self swizzledLaunch];
}
@end
I go into a little more detail on the readme of the demo repository, but the bottom line is that you can switch out a method at runtime with your own... then switch back! This works in combination with manually setting launch environment variables too.
https://github.com/fraune/SwiftUITestDetection
Upvotes: 1
Reputation: 63707
The application's launch within a UITest can only be detected by passing arguments or environment values to XCUIApplication. Since these methods are ruled out by the original question, the answer is that it’s not possible.
As of 2024, the alternative methods listed in other answers do not work. I just lost my time trying them, which goes to show that non Apple approved workarounds are likely to become maintenance problems in the future.
Upvotes: 1
Reputation: 121
Not specific to UI tests, but tests in general, create an extension for ProcessInfo
public extension ProcessInfo {
static var isTesting: Bool {
processInfo.environment["XCTestConfigurationFilePath"] != nil
}
}
Usage:
if ProcessInfo.isTesting {}
guard !ProcessInfo.isTesting else { return }
Upvotes: 1
Reputation: 2797
I didn't succeed with setting a launch environment, but got it to work with launch arguments.
In your tests setUp()
function add:
let app = XCUIApplication()
app.launchArguments = ["testMode"]
app.launch()
In your production code add a check like:
let testMode = NSProcessInfo.processInfo().arguments.contains("testMode")
if testMode {
// Do stuff
}
Verified using Xcode 7.1.1.
Upvotes: 33
Reputation: 16254
My solution is almost identical to that of Ciryon above, except that for my macOS Document-based app I had to prepend a hyphen to the argument name:
let app = XCUIApplication()
app.launchArguments.append("-Testing")
app.launch()
...otherwise, "Testing" ends up interpreted as the name of the document to open when launching the app, so I was getting an error alert:
Upvotes: 1
Reputation: 1634
Swift 3 based on previous answers.
class YourApplicationUITests: XCTestCase {
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
// In UI tests it is usually best to stop immediately when a failure occurs.
continueAfterFailure = false
// UI tests must launch the application that they test. Doing this in setup will make sure it happens for each test method.
let app = XCUIApplication()
app.launchArguments = ["testMode"]
app.launch()
// In UI tests it’s important to set the initial state - such as interface orientation - required for your tests before they run. The setUp method is a good place to do this.
}
override func tearDown() {
// Put teardown code here. This method is called after the invocation of each test method in the class.
super.tearDown()
}
func testExample() {
// Use recording to get started writing UI tests.
// Use XCTAssert and related functions to verify your tests produce the correct results.
}
}
extension UIApplication {
public static var isRunningTest: Bool {
return ProcessInfo().arguments.contains("testMode")
}
}
Then just call UIApplication.isRunningTest in your code.
Upvotes: 6
Reputation: 18157
In Swift 3 you can check for the XCInjectBundleInto
key, or something that starts with XC
.
let isInTestMode = ProcessInfo.processInfo.environment["XCInjectBundleInto"] != nil
This works in OS X as well.
Upvotes: 4
Reputation: 1610
You can use Preprocessor Macros for this. I found that you have couple of choices:
Make a copy of the App's target and use this as the Target to be Tested. Any preproocessor macro in this target copy is accessible in code.
One drawback is you will have to add new classes / resources to the copy target as well and sometimes it very easy to forget.
Make a duplicate of the Debug build configuration , set any preprocessor macro to this configuration and use it for your test (See screenshots below).
A minor gotcha: whenever you want to record a UI Testing session you need to change the Run to use the new testing configuration.
Add a duplicate configuration:
Use it for your Test:
Upvotes: 7
Reputation: 196
I've just added this extension
@available(iOS 9, *)
extension XCUIApplication {
func test(){
launchEnvironment = ["TEST":"true"]
launch()
}
}
So I can just use test() instead of launch()
Upvotes: 3
Reputation: 8785
I've been researching this myself and came across this question. I ended up going with @LironYahdav's first workaround:
In your UI test:
- (void)setUp
{
[super setUp];
XCUIApplication *app = [[XCUIApplication alloc] init];
app.launchEnvironment = @{@"isUITest": @YES};
[app launch];
}
In your app:
NSDictionary *environment = [[NSProcessInfo processInfo] environment];
if (environment[@"isUITest"]) {
// Running in a UI test
}
@JoeMasilotti's solutions are useful for unit tests, because they share the same runtime as the app being tested, but are not relevant for UI tests.
Upvotes: 62