How to speed up UI test cases in Xcode? - ios

Since Xcode 7 we have a nice API for UI testing.
Mostly I'm satisfied with it. The only concern is related to the speed.
In the beginning an ordinary UI test case (about 15 actions) ran approximately 25 seconds. Then I mocked networking completely. Now it takes 20 seconds. Considering the fact that the time is taken only by animations and a launch time (1 second or even less), I assume, there must be a way to speed it up.

Try setting this property when your UI tests run:
UIApplication.shared.keyWindow?.layer.speed = 100
Here's how I set it:
func application(_ application: UIApplication,
didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if ProcessInfo.processInfo.arguments.contains("UITests") {
UIApplication.shared.keyWindow?.layer.speed = 100
}
}
And in my UI tests:
class MyAppUITests: XCTestCase {
// MARK: - SetUp / TearDown
override func setUp() {
super.setUp()
let app = XCUIApplication()
app.launchArguments = ["UITests"]
app.launch()
}
}
There's a few more handy tips in this blog post.

Another possibility is to disable animations at all:
[UIView setAnimationsEnabled:NO];
Swift 3:
UIView.setAnimationsEnabled(false)

Following #Mark answer, the Swift 3 version:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if ProcessInfo.processInfo.arguments.contains("UITests") {
UIApplication.shared.keyWindow?.layer.speed = 200
}
}
On you ui test file:
override func setUp() {
super.setUp()
// Put setup code here. This method is called before the invocation of each test method in the class.
let app = XCUIApplication()
app.launchArguments = ["UITests"]
app.launch()

Add it in didFinishLaunch
[UIApplication sharedApplication].keyWindow.layer.speed = 2;
The default value is 1, make it 2 to double its speed.

Run them in parallel.
EDIT: This may be outdated since my original answer in 2019, since Xcode allows now testing on multiple simulators within one machine:
If you have only 1 build machine, you can use Bluepill: https://github.com/linkedin/bluepill
EDIT: However, I don't use either of these (bluepill / Xcode), so I'll keep mention of the bluepill in this answer, maybe it has some uses.
If you have multiple machines, you can use Emcee: https://github.com/avito-tech/Emcee (it also works for a single machine setup)
We have 50 hours of UI tests, and Emcee allows us to run them in 1 hour. There are several tricks to make UI tests faster, but it is pointless if you are not running them in parallel. E.g. you can't make your 25 seconds tests run in 0.5 second.
Tricks:
Run XCUIApplication without reinstalling it:
XCUIApplication(
privateWithPath: nil,
bundleID: "your.bundle.id"
)
Note: you should launch app with XCUIApplication().launch() at least once per launching XCUI test runner to install the app. This requires usage of private API.
You can move something to background thread. E.g.
let someDataFromApi = getSomeData() // start request asynchronously and immediately return
launchApp() // this can be few seconds
useInTest(someDataFromApi) // access to data will wait request to finish
You can make the code look like there is no asynchronous things, it would be easier for QA. We want to implement this, but we didn't, so it is just an idea.
Switch to EarlGrey and make tests that lasts few seconds (but they will not be black box).
Open screens via deep links if you have deep links.
Use a lot of private API and other hacks. E.g.: instead of going via UI to Settings app and then reset privacy settings you can call some private API.
Never use long sleeps. Use polling.
Speed up animations. Do not disable them! (we use layer.speed = 100 on every view and we got severe problems with the value of 10000, full code: https://pastebin.com/AnsZmzuQ)
Pass some flags from UI tests to app to skip initial alerts/tutorials/popups. Can save a lot of time. Be sure to have at least 1 test that checks that those alerts work.
Advertisement: most of these is implemented in https://github.com/avito-tech/Mixbox. Disclaimer: I'm the main contributor in this test framework. Its tedious to set up, but at least you can reuse some code if you don't want to reuse whole framework.

I wanted to disable ALL animations during Snapshot testing. I was able to able to achieve this by disabling both Core Animation and UIView animations as below.
Note because my app used storyboards UIApplication.shared.keyWindow was nil at launch so I access the UIWindow by referring to the window property directly.
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if ProcessInfo.processInfo.arguments.contains("SnapshotTests") {
// Disable Core Animations
window?.layer.speed = 0
// Disable UIView animations
UIView.setAnimationsEnabled(false)
}
return true
}

I decrease my UITests time in 30%, follow all steps:
When you run your app, add the argument:
let app = XCUIApplication()
override func setUp() {
super.setUp()
continueAfterFailure = false
app.launchArguments += ["--Reset"]
app.launch()
}
Now, in your AppDelegate add:
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplication.LaunchOptionsKey: Any]?) -> Bool {
setStateForUITesting()
}
static var isUITestingEnabled: Bool {
get {
return ProcessInfo.processInfo.arguments.contains("--Reset")
}
}
private func setStateForUITesting() {
if AppDelegate.isUITestingEnabled {
// If you need reset your app to clear state
UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)
// To speed up your tests
UIApplication.shared.keyWindow?.layer.speed = 2
UIView.setAnimationsEnabled(false)
}
}
In your code, to verify if is in test mode, you can use:
if AppDelegate.isUITestingEnabled {
print("Test Mode")
}
Additionally, to can wait while the element load I created this extension:
import XCTest
extension XCUIElement {
func tap(wait: Int, test: XCTestCase) {
if !isHittable {
test.expectation(for: NSPredicate(format: "hittable == true"), evaluatedWith: self, handler: nil);
test.waitForExpectations(timeout: TimeInterval(wait), handler: nil)
}
tap()
}
}
Use like this:
app.buttons["start"].tap(wait: 20, test: self)

Note that keyWindow is deprecated as of iOS 13. Specifically,
UIApplication.sharedApplication.keyWindow and self.window.keyWindow within the AppDelegate both return nil. This answer says to loop through UIApplication.shared.windows and find the one with isKeyWindow true, but in my test using ios14, even that return false for my only window. In the end, setting self.window.layer.speed worked for me.

Related

How to clear NSUserDefaults programmatically in XCUITest using Simulator

I've read several answers related to this and they suggest doing one of the following, but these options are not working for me. I have an XCUITest and I'm trying to clear the standard user defaults before before running the rest of my XCUITest. Currently my test app has a button that calls this code. I've also tried calling this code directly from within the XCUITest (I'm not sure if this is expected to work or if it needs to be run from within the app).
NSString *appDomain = [[NSBundle mainBundle] bundleIdentifier];
[[NSUserDefaults standardUserDefaults] removePersistentDomainForName:appDomain];
I've also tried removing each individually:
[[NSUserDefaults standardUserDefaults] removeObjectForKey:#"MyKey1"];
[[NSUserDefaults standardUserDefaults] removeObjectForKey:#"MyKey2"];
I also tried each of the above methods followed by a synchronize call:
[[NSUserDefaults standardUserDefaults] synchronize];
The next time I read #"MyKey1" from the NSUserDefaults its still has the old value and has not been deleted.
Is there any way to remove an object from the NSUserDefaults programmatically when running an XCUITest in the simulator? These are automated tests, so I can't always manually click on "Reset Contents and Settings" in xcode.
Thanks!
There are couple of ways to solve your issues by setting the UserDefaults values before your tests run the app (not after).
Solution #1
Mock UserDefaults values for certain keys using launchArguments:
func testExample() {
let app = XCUIApplication()
app.launchArguments += ["-keepScreenOnKey", "YES"]
app.launch()
}
Note the minus sign before the keepScreenOnKey key. The minus sign indicates that it should take the next launch argument value as a value for that UserDefaults key.
Solution #2 (if solution #1 doesn’t work)
The previous solution might not work if you’re using the SwiftyUserDefaults library. In this case, there is one elegant solution: if the application is running under UI tests, erase all UserDefaults data from the app. How does the app know if it is being run under UI tests? By passing the launch arguments message, like this:
override func setUp() {
super.setUp()
app.launchArguments += ["UI-Testing"]
}
Then, in AppDelegate.swift check if the app is running under UI-Testing and remove all UserDefaults data (like you do):
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
setStateForUITesting()
return true
}
static var isUITestingEnabled: Bool {
get {
return ProcessInfo.processInfo.arguments.contains("UI-Testing")
}
}
private func setStateForUITesting() {
if AppDelegate.isUITestingEnabled {
UserDefaults.standard.removePersistentDomain(forName: Bundle.main.bundleIdentifier!)
}
}
}
Solution #3 (if solution #2 is not enough)
But what if the UI test expects states other than the default state set by solution #2? Bools default to false, Integers to 0 and Strings to "", but what if you need true for the Bool key?
Do something like this:
func testExample() {
app.launchEnvironment["UI-TestingKey_keepScreenOn"] = "YES"
app.launch()
}
And then in AppDelegate.swift file:
import UIKit
#UIApplicationMain
class AppDelegate: UIResponder, UIApplicationDelegate {
var window: UIWindow?
static let uiTestingKeyPrefix = "UI-TestingKey_"
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
if AppDelegate.isUITestingEnabled {
setUserDefaults()
}
return true
}
static var isUITestingEnabled: Bool {
get {
return ProcessInfo.processInfo.arguments.contains("UI-Testing")
}
}
private func setUserDefaults() {
for (key, value)
in ProcessInfo.processInfo.environment
where key.hasPrefix(AppDelegate.uiTestingKeyPrefix) {
// Truncate "UI-TestingKey_" part
let userDefaultsKey = key.truncateUITestingKey()
switch value {
case "YES":
UserDefaults.standard.set(true, forKey: userDefaultsKey)
case "NO":
UserDefaults.standard.set(false, forKey: userDefaultsKey)
default:
UserDefaults.standard.set(value, forKey: userDefaultsKey)
}
}
}
}
extension String {
func truncateUITestingKey() -> String {
if let range = self.range(of: AppDelegate.uiTestingKeyPrefix) {
let userDefaultsKey = self[range.upperBound...]
return String(userDefaultsKey)
}
return self
}
}
Please note that this example only works for Bool and String keys. If you need more scalability, the switch command should be modified to somehow check if the value is Integer or Double or Any other value, but the general idea is here.
EDIT: It looks like the reason for using solutions #2 and #3 is not valid anymore as of SwiftyUserDefaults version 4.0.0-beta.1 as they've added support for setting values through launch arguments. But, I have to admit that I have not tested SwiftyUserDefaults library from this version onward, so I'll keep both solutions here.
With UI tests, the app runs as a separate process. You would need to call the methods to clear NSUserDefaults from within the app itself.
We have our UI Test pass a resetNSUserDefaults flag to the app when it launches the app. The app then clears the NSUserDefaults early in the launch process.

SKStoreReviewController is being displayed the first time user launches the app

In my iOS app I'm using SKStoreReviewController to request users to rate the app. The Apple documentation says to put the code for requesting the "Rate Us" popup anywhere we want, and they will govern when it will be actually displayed.
I wrote the following code in the first view of the app:
func requestReview() {
SKStoreReviewController.requestReview()
}
The problem is that the popup is displayed to my app's users as soon as they first launch the app, which makes no sense. Is there any way to control the popup's appearance and avoid showing it before certain amount of uses of the app?
SKStoreReviewController.requestReview() will display the popup for the first few times (to be exact, for the first 3 times in a year).
Create a variable that you increment each time in your application delegate's didFinishLaunchingWithOptions method and save it to UserDefaults. After that, you can check whether a user opened the app enough times.
AppDelegate
func application(_ application: UIApplication, didFinishLaunchingWithOptions launchOptions: [UIApplicationLaunchOptionsKey: Any]?) -> Bool {
var appLaunches = UserDefaults.standard.integer(forKey: "appLaunches")
appLaunches += 1
UserDefaults.standard.set(appLaunches, forKey: "appLaunches")
return true
}
The view controller where you want to display the store review controller
let appLaunches = UserDefaults.standard.integer(forKey: "appLaunches")
if appLaunches >= [enough number of app launches] {
SKStoreReviewController.requestReview()
}

iOS 9 Quick Actions (3D Touch)

I am trying to understand doing Quick Actions (3D Touch) for iOS 9.
I wanted the user to select 1 of 4 filter to be applied to image, so if I select item 1, I will set the NSUserDefaults.standardUserDefaults() to the filter, then show the correct picture with the applied filter.
In AppDelete.swift:
func application(application: UIApplication, performActionForShortcutItem shortcutItem: UIApplicationShortcutItem, completionHandler: (Bool) -> Void) {
var filterType:Int
switch (shortcutItem.type) {
...set filterType
}
NSUserDefaults.standardUserDefaults().setInteger(filterType, forKey:"filterType")
NSUserDefaults.standardUserDefaults().synchronize()
}
In ViewController.swift:
override func viewDidLoad() {
super.viewDidLoad()
NSNotificationCenter.defaultCenter().addObserver(self, selector:"setDefaultFilter", name: UIApplicationWillEnterForegroundNotification, object:nil) // Handle enter from background
setDefaultFilter()
}
func setDefaultFilter() {
filterType = defaults.integerForKey("filterType")
...
imageView.image = filterImage(defaultImage!, filter:filters[filterType])
}
However, when enter the app from the menu, it will always show the last selection (not the current selection). If I select item 1, nothing happened. I select item 3, item 1 will appeared.
I have also try passing parameters via appDelegate and the result is the same. I believe there are some issues with life cycle.
Any ideas?
NSUserDefaults write data on flash, which may not be so fast.
You can wait a little longer, like observe UIApplicationDidBecomeActiveNotification other than UIApplicationWillEnterForegroundNotification.
Or you can use other ways to pass params, e.g., as an instance variable in AppDelegate.
didFinishLaunchingWithOptions method is always called before calling performActionForShortcutItem method to response to the quick action.
So, I think that you need to check what kind of quick action is selected in didFinishLaunchingWithOptions method. If the app is not launched from quick action, you just continue to your normal app launching process.(default filter)
And if you decide to handle quick action in didFinishLaunchingWithOptions, you have to return NO in didFinishLaunchingWithOptions.
You could get more idea from my demo project:
https://github.com/dakeshi/3D_Touch_HomeQuickAction

Is it possible to stub HTTP requests in Xcode 7 automated UI tests?

I've been trying to intercept and stub/mock HTTP requests in Xcode 7 automated UI tests, using tools like OHHTTPStubs, with no luck.
Here's an example of how I am trying to capture any HTTP request using OHHTTPStubs in the setUp method of a UI test file:
override func setUp() {
super.setUp()
let matcher: OHHTTPStubsTestBlock = { (request) -> Bool in
return true
}
OHHTTPStubs.stubRequestsPassingTest(matcher) { (response) -> OHHTTPStubsResponse! in
return OHHTTPStubsResponse.init()
}
}
Is there something about the way that UI testing works that prevents this? has anyone been able to achieve this?
As Martijn correctly pointed out, because of how UI tests work, you cannot directly interact with the app at runtime, so any HTTP mocking or manipulation of things like NSUserDefaults in a XCUITestCase will not affect your app.
If you really need to be able to mock HTTP or setup & teardown your apps environment for specific UI tests, you will need to set launch arguments or launch environment variables before launching the app in the setUp() method of a XCUITestCase and then modify your app code to read the launch arguments or environment variables and bootstrap the test environment.
Example TestCase
class MyTestCase: XCTestCase {
/**
Called before each test in this test case.
*/
override func setUp() {
super.setUp()
let app = XCUIApplication()
app.launchArguments = [ "STUB_HTTP_ENDPOINTS" ]
app.launch()
}
}
Example AppDelegate
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
#if DEBUG
if (NSProcessInfo.processInfo().arguments.contains("STUB_HTTP_ENDPOINTS")) {
// setup HTTP stubs for tests
}
#endif
return true
}
Note: In order to use an HTTP mocking framework like OHHTTPStubs in this example, the stubbing code and any JSON fixtures you need to use will all need to be in your app target, not the test target.
This is a very useful thread to read on the topic: https://github.com/AliSoftware/OHHTTPStubs/issues/124
UI tests are ran in a separate instance from your application. While the classes from the application might be available to you, they are merely a copy.
In your application you can detect if you're running in UI testing mode with solutions provided here: How to detect if iOS app is running in UI Testing mode
I personally went with the launchEnvironment solution mentioned in the original post; my setUp looks like this:
override func setUp() {
super.setUp()
let app = XCUIApplication()
app.launchEnvironment["TEST"] = "1"
app.launch()
}
And one of my singleton instantiators (called RealmManager) looks like this (for instantiating a Realm database):
func realm() -> Realm {
let dic = NSProcessInfo.processInfo().environment
if dic["TEST"] != nil {
return try! Realm(configuration: Realm.Configuration(inMemoryIdentifier: "test"))
}
return try! Realm()
}
If you dislike the duplication, but you're probably already duplicating XCUIApplication().launch() anyway, you can always make a custom test case class that extend XCTestCase, override the setUp there with this addition and then use that in all your test classes.

Apple HealthKit - background updates not triggering

I am trying to perform some actions triggered by changes to Apple Health Kit, triggered in the background of my Swift iOS app.
Here's my AppDelegate:
var healthManager : HealthManager?
func application(application: UIApplication, didFinishLaunchingWithOptions launchOptions: [NSObject: AnyObject]?) -> Bool {
healthManager = HealthManager.sharedInstance
return true
}
And in the initialization of the HealthManager class I authorize use of Health Kit and call:
var sampleType = HKQuantityType.quantityTypeForIdentifier(HKQuantityTypeIdentifierStepCount)
var predicate = HKQuery.predicateForSamplesWithStartDate(startDate, endDate: endDate, options: HKQueryOptions.StrictStartDate)
var query = HKObserverQuery(sampleType: sampleType, predicate: predicate, updateHandler: stepsChangedHandler)
healthKitStore.executeQuery(query)
healthKitStore.enableBackgroundDeliveryForType(sampleType, frequency: .Immediate, withCompletion: {(succeeded, error) in
if succeeded {
println("Enabled background delivery of step changes")
} else {
if let theError = error {
print("Failed to enable background delivery of step changed. ")
println("Error = \(theError)")
}
}
})
This works beautifully when the app is open- the stepsChangedHandler is called when there's a health kit update, but when the app is out of focus it never is called. I've searched around and found a few ideas, but none of the fixes have seemed to work for me.
Thanks!
What you have should work, but my experience with the simulator up through iOS 8.4 and Xcode 6.4 is that the background updates are not triggered. However, in my testing this does work on a device. To try for yourself, hook up and run your app on a device then switch to Health.app and add a relevant data point.
If your query is set for immediate updates, you should see your log message in the console. Make sure stepsChangedHandler includes completionHandler().
According to the docs, the query runs on a separate background thread so your appdelegate code will only be called on initial launch.
In the documentation for the HKHealthStore Class, under enableBackgroundDeliveryForType:... there is a paragraph:
Some data types, such as step counts, have a minimum frequency of HKUpdateFrequencyHourly. This frequency is enforced transparently.
which explains why you won't see background updates as frequently as you are specifying. I'm not sure if theres a listing of which data types are included in the "some" quantifier.

Resources