UIAutomation Launch Arguments

I have long been a fan of UI Automation tests. When done right and focusing on the behavior of the app they are an extremely useful tool. UIAutomation testing along side unit and snapshot testing enables BOLD refactoring and I like that. Having a lot of confidence in the codebase is never a bad thing.

Testing from the black box

Apples approach to force us to do black box testing during automation tests has its merits. In theory keeping all testing code away from the production code is exactly what we should be striving for. In a lot of common use cases this is perfectly acceptable, however inevitably the time comes when this will no longer work. In my experience the two most frustrating things in modern automation testing (aside from flakeyness) are;

  • Data is being written to some persistent store (Defaults, Keychain, Core Data etc)
  • The test suite has grown large and is taking too long.

A common way to deal with the first problem is too reset the application on each test launch. This action needs to be performed from the main target. The second problem can actually be quite significantly improved by increasing the speed of the layer animations in app. This again is performed in the main target. Notice the theme? there are legitimate cases where we need to communicate with the main target whilst running an automation suite.

Communicating with the main target

The way most developers communicate with the main target is through launch arguments. We can set launch arguments from our test target like so


let app = XCUIApplication()
app.launchArguments = ["--Reset", "--IncreaseLayerSpeed"]
app.launch()

Then in our application delegate (usually in didFinishLaunchingWithOptions) we check ProcessInfo for these launch arguments


if ProcessInfo.processInfo.arguments.contains("--Reset") {
  //Do Something
}

if ProcessInfo.processInfo.arguments.contains("--IncreaseLayerSpeed") {
  //Do Something
}

Whilst this is all perfectly fine all the "stringly typedness" makes me nervous.

Making it safe

I nice solution I have found is to wrap all the possible launch arguments up in an enum and add a couple of helper extensions to make everything type safe. First we define an ApplicationLaunchArguments enum which is included in the main and testing target


enum ApplicationLaunchArgument: String {
  case Reset = "--ResetData"
  case IncreaseLayerSpeed = "--IncreaseLayerSpeed"
}

With this in place we now extend XCUIApplication with a helper launch method


extension XCUIApplication {
  func launchWithArguments(launchArguments: [ApplicationLaunchArgument]) {
    let launchArgumentStrings = launchArguments.map { $0.rawValue }
    self.launchArguments = launchArgumentStrings
    launch()
  }
}

Finally we extend ProcessInfo with a helper that checks for the presence of one of these enums


extension ProcessInfo {
  static func launchArgumentPresent(_ launchArgument: ApplicationLaunchArgument) -> Bool {
    return processInfo.arguments.contains(launchArgument.rawValue)
  }
}

What it looks like now

In our test code we can now launch the app with type safe launch arguments like so


let app = XCUIApplication()
app.launchWithArguments(launchArguments: [.Reset, .IncreaseLayerSpeed])

and in our app delegate we can query for the presence of these arguments like so


if ProcessInfo.launchArgumentPresent(.Reset) {
  //Do Something
}

if ProcessInfo.launchArgumentPresent(.IncreaseLayerSpeed) {
  //Do Something
}

Conclusion

Although it may not look like we have gained a lot, it is a nice quick simple way to just make the codebase that little bit more solid and scalable. If you are passing launch arguments I encourage you to take an approach like this, it doesn't cost a lot and mistypes are immediately obvious.