Is there a way to reset the app between tests in Swift XCTest UI?

不问归期 提交于 2019-12-17 08:05:19

问题


Is there an API call within XCTest that I can put into the setUP() or tearDown() to reset the app between tests? I looked in the dot syntax of XCUIApplication and all I saw was the .launch()

OR is there a way to call a shell script in Swift? I could then call xcrun in-between test methods to reset the simulator.


回答1:


You can add a "Run Script" phase to build phases in your test target to uninstall the app before running unit tests against it, unfortunately this is not between test cases, though.

/usr/bin/xcrun simctl uninstall booted com.mycompany.bundleId

Update


Between tests, you can delete the app via the Springboard in the tearDown phase. Although, this does require use of a private header from XCTest. (Header dump is available from Facebook's WebDriverAgent here.)

Here is some sample code from a Springboard class to delete an app from Springboard via tap and hold:

Swift 4:

import XCTest

class Springboard {

    static let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")

    /**
     Terminate and delete the app via springboard
     */
    class func deleteMyApp() {
        XCUIApplication().terminate()

         // Force delete the app from the springboard
        let icon = springboard.icons["Citizen"]
        if icon.exists {
            let iconFrame = icon.frame
            let springboardFrame = springboard.frame
            icon.press(forDuration: 1.3)

            // Tap the little "X" button at approximately where it is. The X is not exposed directly
            springboard.coordinate(withNormalizedOffset: CGVector(dx: (iconFrame.minX + 3) / springboardFrame.maxX, dy: (iconFrame.minY + 3) / springboardFrame.maxY)).tap()

            springboard.alerts.buttons["Delete"].tap()
        }
    }
 }

Swift 3-:

import XCTest

class Springboard {

    static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")

    /**
     Terminate and delete the app via springboard
     */
    class func deleteMyApp() {
        XCUIApplication().terminate()

        // Resolve the query for the springboard rather than launching it
        springboard.resolve()

        // Force delete the app from the springboard
        let icon = springboard.icons["MyAppName"]
        if icon.exists {
            let iconFrame = icon.frame
            let springboardFrame = springboard.frame
            icon.pressForDuration(1.3)

            // Tap the little "X" button at approximately where it is. The X is not exposed directly
            springboard.coordinateWithNormalizedOffset(CGVectorMake((iconFrame.minX + 3) / springboardFrame.maxX, (iconFrame.minY + 3) / springboardFrame.maxY)).tap()

            springboard.alerts.buttons["Delete"].tap()
        }
    }
 }

And then:

override func tearDown() {
    Springboard.deleteMyApp()
    super.tearDown()
}

The private headers were imported in the Swift bridging header. You'll need to import:

// Private headers from XCTest
#import "XCUIApplication.h"
#import "XCUIElement.h"

Note: As of Xcode 10, XCUIApplication(bundleIdentifier:) is now exposed by Apple, and the private headers are no longer needed.




回答2:


At this time, the public API in Xcode and the Simulator does not appear have any method callable from setUp() and tearDown() XCText subclasses to "Reset Contents and Settings" for the simulator.

There are other possible approaches which use public APIs:

  1. Application Code. Add some myResetApplication() application code to put the application in a known state. However, device (simulator) state control is limited by the application sandbox ... which is not much help outside the application. This approach is OK for clearing application controllable persistance.

  2. Shell Script. Run the tests from a shell script. Use xcrun simctl erase all or xcrun simctl uninstall <device> <app identifier> or similar between each test run to reset the simulator (or uninstall the app). see StackOverflow: "How can I reset the iOS Simulator from the command line?"

macos> xcrun simctl --help
# can uninstall a single application
macos> xcrun simctl uninstall --help  
# Usage: simctl uninstall <device> <app identifier>
  1. Xcode Schema Action. Add xcrun simctl erase all (or xcrun simctl erase <DEVICE_UUID>) or similar to the Scheme Test section. Select the Product > Scheme > Edit Scheme… menu. Expand the Scheme Test section. Select Pre-actions under the Test section. Click (+) add "New Run Script Action". The command xcrun simctl erase all can be typed in directly without requiring any external script.

Options for invoking 1. Application Code to reset the application:

A. Application UI. [UI Test] Provide a reset button or other UI action which resets the application. The UI element can be exercised via XCUIApplication in XCTest routines setUp(), tearDown() or testSomething().

B. Launch Parameter. [UI Test] As noted by Victor Ronin, an argument can be passed from the test setUp() ...

class AppResetUITests: XCTestCase {

  override func setUp() {
    // ...
    let app = XCUIApplication()
    app.launchArguments = ["MY_UI_TEST_MODE"]
    app.launch()

... to be received by the AppDelegate ...

class AppDelegate: UIResponder, UIApplicationDelegate {

  func application( …didFinishLaunchingWithOptions… ) -> Bool {
    // ...
    let args = NSProcessInfo.processInfo().arguments
    if args.contains("MY_UI_TEST_MODE") {
      myResetApplication()
    }

C. Xcode Scheme Parameter. [UI Test, Unit Test] Select the Product > Scheme > Edit Scheme… menu. Expand the Scheme Run section. (+) Add some parameter like MY_UI_TEST_MODE. The parameter will be available in NSProcessInfo.processInfo().

// ... in application
let args = NSProcessInfo.processInfo().arguments
if args.contains("MY_UI_TEST_MODE") {
    myResetApplication()
}

Z. Direct Call. [Unit Test] Unit Test Bundles are injected into the running application and can directly call some myResetApplication() routine in the application. Caveat: Default unit tests run after the main screen has loaded. see Test Load Sequence However, UI Test Bundles runs as a process external to the application under test. So, what works in the Unit Test gives a link error in a UI Test.

class AppResetUnitTests: XCTestCase {

  override func setUp() {
    // ... Unit Test: runs.  UI Test: link error.
    myResetApplication() // visible code implemented in application



回答3:


Updated for swift 3.1 / xcode 8.3

create bridging header in test target:

#import <XCTest/XCUIApplication.h>
#import <XCTest/XCUIElement.h>

@interface XCUIApplication (Private)
- (id)initPrivateWithPath:(NSString *)path bundleID:(NSString *)bundleID;
- (void)resolve;
@end

updated Springboard class

class Springboard {
   static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")!
   static let settings = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.Preferences")!

/**
Terminate and delete the app via springboard
*/

class func deleteMyApp() {
   XCUIApplication().terminate()

// Resolve the query for the springboard rather than launching it

   springboard.resolve()

// Force delete the app from the springboard
   let icon = springboard.icons["{MyAppName}"] /// change to correct app name
   if icon.exists {
     let iconFrame = icon.frame
     let springboardFrame = springboard.frame
     icon.press(forDuration: 1.3)

  // Tap the little "X" button at approximately where it is. The X is not exposed directly

    springboard.coordinate(withNormalizedOffset: CGVector(dx: (iconFrame.minX + 3) / springboardFrame.maxX, dy: (iconFrame.minY + 3) / springboardFrame.maxY)).tap()

     springboard.alerts.buttons["Delete"].tap()

     // Press home once make the icons stop wiggling

     XCUIDevice.shared().press(.home)
     // Press home again to go to the first page of the springboard
     XCUIDevice.shared().press(.home)
     // Wait some time for the animation end
     Thread.sleep(forTimeInterval: 0.5)

      let settingsIcon = springboard.icons["Settings"]
      if settingsIcon.exists {
       settingsIcon.tap()
       settings.tables.staticTexts["General"].tap()
       settings.tables.staticTexts["Reset"].tap()
       settings.tables.staticTexts["Reset Location & Privacy"].tap()
       settings.buttons["Reset Warnings"].tap()
       settings.terminate()
      }
     }
    }
   }



回答4:


You can ask your app to "clean up" itself

  • You use XCUIApplication.launchArguments to set some flag
  • In AppDelegate you check

    if NSProcessInfo.processInfo().arguments.contains("YOUR_FLAG_NAME_HERE") { // Do a clean up here }




回答5:


I used the @ODM answer, but modified it to work for Swift 4. NB: some S/O answers don't differentiate the Swift versions, which sometimes have fairly fundamental differences. I've tested this on an iPhone 7 simulator and an iPad Air simulator in portrait orientation, and it worked for my app.

Swift 4

import XCTest
import Foundation

class Springboard {

let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")
let settings = XCUIApplication(bundleIdentifier: "com.apple.Preferences")


/**
 Terminate and delete the app via springboard
 */
func deleteMyApp() {
    XCUIApplication().terminate()

    // Resolve the query for the springboard rather than launching it
    springboard.activate()

    // Rotate back to Portrait, just to ensure repeatability here
    XCUIDevice.shared.orientation = UIDeviceOrientation.portrait
    // Sleep to let the device finish its rotation animation, if it needed rotating
    sleep(2)

    // Force delete the app from the springboard
    // Handle iOS 11 iPad 'duplication' of icons (one nested under "Home screen icons" and the other nested under "Multitasking Dock"
    let icon = springboard.otherElements["Home screen icons"].scrollViews.otherElements.icons["YourAppName"]
    if icon.exists {
        let iconFrame = icon.frame
        let springboardFrame = springboard.frame
        icon.press(forDuration: 2.5)

        // Tap the little "X" button at approximately where it is. The X is not exposed directly
        springboard.coordinate(withNormalizedOffset: CGVector(dx: ((iconFrame.minX + 3) / springboardFrame.maxX), dy:((iconFrame.minY + 3) / springboardFrame.maxY))).tap()
        // Wait some time for the animation end
        Thread.sleep(forTimeInterval: 0.5)

        //springboard.alerts.buttons["Delete"].firstMatch.tap()
        springboard.buttons["Delete"].firstMatch.tap()

        // Press home once make the icons stop wiggling
        XCUIDevice.shared.press(.home)
        // Press home again to go to the first page of the springboard
        XCUIDevice.shared.press(.home)
        // Wait some time for the animation end
        Thread.sleep(forTimeInterval: 0.5)

        // Handle iOS 11 iPad 'duplication' of icons (one nested under "Home screen icons" and the other nested under "Multitasking Dock"
        let settingsIcon = springboard.otherElements["Home screen icons"].scrollViews.otherElements.icons["Settings"]
        if settingsIcon.exists {
            settingsIcon.tap()
            settings.tables.staticTexts["General"].tap()
            settings.tables.staticTexts["Reset"].tap()
            settings.tables.staticTexts["Reset Location & Privacy"].tap()
            // Handle iOS 11 iPad difference in error button text
            if UIDevice.current.userInterfaceIdiom == .pad {
                settings.buttons["Reset"].tap()
            }
            else {
                settings.buttons["Reset Warnings"].tap()
            }
            settings.terminate()
        }
    }
  }
}



回答6:


I used the @Chase Holland answer and updated the Springboard class following the same approach to reset the content and settings using the Settings app. This is useful when you need to reset permissions dialogs.

import XCTest

class Springboard {
    static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")
    static let settings = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.Preferences")

    /**
     Terminate and delete the app via springboard
     */
    class func deleteMyApp() {
        XCUIApplication().terminate()

        // Resolve the query for the springboard rather than launching it
        springboard.resolve()

        // Force delete the app from the springboard
        let icon = springboard.icons["MyAppName"]
        if icon.exists {
            let iconFrame = icon.frame
            let springboardFrame = springboard.frame
            icon.pressForDuration(1.3)

            // Tap the little "X" button at approximately where it is. The X is not exposed directly
            springboard.coordinateWithNormalizedOffset(CGVectorMake((iconFrame.minX + 3) / springboardFrame.maxX, (iconFrame.minY + 3) / springboardFrame.maxY)).tap()

            springboard.alerts.buttons["Delete"].tap()

            // Press home once make the icons stop wiggling
            XCUIDevice.sharedDevice().pressButton(.Home)
            // Press home again to go to the first page of the springboard
            XCUIDevice.sharedDevice().pressButton(.Home)
            // Wait some time for the animation end
            NSThread.sleepForTimeInterval(0.5)

            let settingsIcon = springboard.icons["Settings"]
            if settingsIcon.exists {
                settingsIcon.tap()
                settings.tables.staticTexts["General"].tap()
                settings.tables.staticTexts["Reset"].tap()
                settings.tables.staticTexts["Reset Location & Privacy"].tap()
                settings.buttons["Reset Warnings"].tap()
                settings.terminate()
            }
        }
    }
}



回答7:


For iOS 11 sims an up, I made an ever so slight modification to tap the "x" icon and where we tap per the fix @Code Monkey suggested. Fix works well on both 10.3 and 11.2 phone sims. For the record, I'm using swift 3. Thought i'd through some code out there to copy and paste to find the fix a little easier. :)

import XCTest

class Springboard {

    static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")

    class func deleteMyApp() {
        XCUIApplication().terminate()

        // Resolve the query for the springboard rather than launching it
        springboard!.resolve()

        // Force delete the app from the springboard
        let icon = springboard!.icons["My Test App"]
        if icon.exists {
            let iconFrame = icon.frame
            let springboardFrame = springboard!.frame
            icon.press(forDuration: 1.3)

            springboard!.coordinate(withNormalizedOffset: CGVector(dx: (iconFrame.minX + 3 * UIScreen.main.scale) / springboardFrame.maxX, dy: (iconFrame.minY + 3 * UIScreen.main.scale) / springboardFrame.maxY)).tap()

            springboard!.alerts.buttons["Delete"].tap()
        }
    }
}



回答8:


This seems to work for me on iOS 12.1 & simulator

class func deleteApp(appName: String) {
    XCUIApplication().terminate()

    // Force delete the app from the springboard
    let icon = springboard.icons[appName]
    if icon.exists {
        icon.press(forDuration: 2.0)

        icon.buttons["DeleteButton"].tap()
        sleep(2)
        springboard.alerts["Delete “\(appName)”?"].buttons["Delete"].tap()
        sleep(2)

        XCUIDevice.shared.press(.home)
    }
}



回答9:


iOS 13.1/Swift 5.1 UI based deletion

static let springboard = XCUIApplication(privateWithPath: nil, bundleID: "com.apple.springboard")!

class func deleteApp() {
    XCUIApplication().terminate()
    XCUIDevice.shared.press(.home)
    XCUIDevice.shared.press(.home)

    let icon = springboard.icons["YourApplication"]
    if !icon.exists { return }

    springboard.swipeLeft()
    springboard.activate()
    Thread.sleep(forTimeInterval: 1.0)

    icon.press(forDuration: 1.3)
    springboard.buttons["Rearrange Apps"].eventuallyExists().tap()

    icon.buttons["DeleteButton"].eventuallyExists().tap()
    springboard.alerts.buttons["Delete"].eventuallyExists().tap()

    XCUIDevice.shared.press(.home)
    XCUIDevice.shared.press(.home)
}



回答10:


Solution for iOS 13.2

final class Springboard {

    private static var springboardApp = XCUIApplication(bundleIdentifier: "com.apple.springboard")

    class func deleteApp(name: String) {
        XCUIApplication().terminate()

        springboardApp.activate()

        sleep(1)

        let appIcon = springboardApp.icons.matching(identifier: name).firstMatch
        appIcon.press(forDuration: 1.3)

        sleep(1)

        springboardApp.buttons["Delete App"].tap()

        let deleteButton = springboardApp.alerts.buttons["Delete"].firstMatch
        if deleteButton.waitForExistence(timeout: 5) {
            deleteButton.tap()
        }
    }
}



回答11:


Updating Craig Fishers answer for Swift 4. Updated for iPad in landscape, probably only works for landscape left.

import XCTest

class Springboard {

static let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")

class func deleteMyApp(name: String) {        
    // Force delete the app from the springboard
    let icon = springboard.icons[name]
    if icon.exists {
        let iconFrame = icon.frame
        let springboardFrame = springboard.frame
        icon.press(forDuration: 2.0)

        var portaitOffset = 0.0 as CGFloat
        if XCUIDevice.shared.orientation != .portrait {
            portaitOffset = iconFrame.size.width - 2 * 3 * UIScreen.main.scale
        }

        let coord = springboard.coordinate(withNormalizedOffset: CGVector(dx: (iconFrame.minX + portaitOffset + 3 * UIScreen.main.scale) / springboardFrame.maxX, dy: (iconFrame.minY + 3 * UIScreen.main.scale) / springboardFrame.maxY))
        coord.tap()

        let _ = springboard.alerts.buttons["Delete"].waitForExistence(timeout: 5)
        springboard.alerts.buttons["Delete"].tap()

        XCUIDevice.shared.press(.home)
    }
}

}




回答12:


Here is an Objective C version of the above answers to delete an App and reset warnings (tested on iOS 11 & 12):

- (void)uninstallAppNamed:(NSString *)appName {

    [[[XCUIApplication alloc] init] terminate];

    XCUIApplication *springboard = [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.springboard"];
    [springboard activate];
    XCUIElement *icon = springboard.otherElements[@"Home screen icons"].scrollViews.otherElements.icons[appName];

    if (icon.exists) {
        [icon pressForDuration:2.3];
        [icon.buttons[@"DeleteButton"] tap];
        sleep(2);
        [[springboard.alerts firstMatch].buttons[@"Delete"] tap];
        sleep(2);
        [[XCUIDevice sharedDevice] pressButton:XCUIDeviceButtonHome];
        sleep(2);
    }
}

..

- (void)resetWarnings {

    XCUIApplication *settings = [[XCUIApplication alloc] initWithBundleIdentifier:@"com.apple.Preferences"];
    [settings activate];
    sleep(2);
    [settings.tables.staticTexts[@"General"] tap];
    [settings.tables.staticTexts[@"Reset"] tap];
    [settings.tables.staticTexts[@"Reset Location & Privacy"] tap];

    if (UI_USER_INTERFACE_IDIOM() == UIUserInterfaceIdiomPad) {
        [settings.buttons[@"Reset"] tap];
    } else {
        [settings.buttons[@"Reset Warnings"] tap];
    }
    sleep(2);
    [settings terminate];
}



回答13:


This works for me in all OS version(iOS11,12 & 13)

static let springboard = XCUIApplication(bundleIdentifier: "com.apple.springboard")

    func deleteApp() {
        XCUIApplication().terminate()
        springboard.activate()

        let icon = springboard.icons[appName]

        if icon.exists {
            icon.firstMatch.press(forDuration: 5)
            icon.buttons["DeleteButton"].tap()

            let deleteConfirmation = springboard.alerts["Delete “\(appName)”?"].buttons["Delete"]
            XCTAssertTrue(deleteConfirmation.waitForExistence(timeout: 5), "Delete confirmation not shown")
            deleteConfirmation.tap()
        }
    }



回答14:


I see a lot of answers to uninstall your app in setUp or tearDown of your test.

But you can easily uninstall your app before launching your tests by adding a run script phase in your test target.

To do so :

  1. Select your application's Xcode project
  2. Select your test target
  3. Select "Build Phases"
  4. Tap on "+" and "New Run Script Phase"

Then, replace the placeholder # Type a script or drag a script file from your workspace to insert its path. by the command :

xcrun simctl boot ${TARGET_DEVICE_IDENTIFIER}
xcrun simctl uninstall ${TARGET_DEVICE_IDENTIFIER} YOUR_APP_BUNDLE


来源:https://stackoverflow.com/questions/33107731/is-there-a-way-to-reset-the-app-between-tests-in-swift-xctest-ui

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!