web-dev-qa-db-fra.com

Existe-t-il un moyen de réinitialiser l'application entre les tests dans Swift XCTest UI?

Existe-t-il un appel d'API dans XCTest que je peux mettre dans setUP () ou tearDown () pour réinitialiser l'application entre les tests? J'ai regardé dans la syntaxe de points de XCUIApplication et tout ce que j'ai vu était le .launch ()

OU y a-t-il un moyen d'appeler un script shell dans Swift? Je pourrais ensuite appeler xcrun entre deux méthodes de test pour réinitialiser le simulateur.

53
JJacquet

Vous pouvez ajouter une phase "Exécuter le script" pour créer des phases dans votre cible de test afin de désinstaller l'application avant d'exécuter des tests unitaires. malheureusement ce n'est pas entre les cas de test, bien que.

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

Mettre à jour


Entre les tests, vous pouvez supprimer l'application via le Springboard dans la phase d'arrachement. Bien que cela nécessite l'utilisation d'un en-tête privé de XCTest. (Le vidage d'en-tête est disponible à partir de WebDriverAgent de Facebook ici .)

Voici un exemple de code d'une classe Springboard permettant de supprimer une application de Springboard en maintenant le doigt appuyé:

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()
        }
    }
 }

Et alors:

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

Les en-têtes privés ont été importés dans l'en-tête de pontage rapide. Vous aurez besoin d'importer:

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

Note: à partir de Xcode 10, XCUIApplication(bundleIdentifier:) est maintenant exposé par Apple et les en-têtes privés sont n'est plus nécessaire.

59
Chase Holland

Actuellement, les API publique de Xcode 7 et 8 et le simulateur ne semblent pas avoir de méthode appelable à partir des sous-classes setUp() et tearDown()XCText pour "Réinitialiser le contenu et les paramètres" du simulateur. 

Il existe d'autres approches possibles utilisant des API publiques:

  1. Code d'application. Ajoutez du code d'application myResetApplication() pour mettre l'application dans un état connu. Cependant, le contrôle de l'état du périphérique (simulateur) est limité par le bac à sable de l'application ... qui n'est pas d'une grande aide en dehors de l'application. Cette approche convient pour effacer la persistance contrôlable par l'application.

  2. Script Shell. Exécutez les tests à partir d'un script Shell. Utilisez xcrun simctl erase all ou xcrun simctl uninstall <device> <app identifier> ou similaire entre chaque série de tests pour réinitialiser le simulateur (ou désinstaller l'application). voir StackOverflow: "Comment puis-je réinitialiser le simulateur iOS à partir de la ligne de commande?"

    macos> xcrun simctl --help
    # can uninstall a single application
    macos> xcrun simctl uninstall --help  
    # Usage: simctl uninstall <device> <app identifier>
  1. Xcode Schema Action. Ajoutez xcrun simctl erase all (ou xcrun simctl erase <DEVICE_UUID>) ou similaire à la section Test de schéma. Sélectionnez le menu Product> Scheme> Edit Scheme…. Développez la section Test de schéma. Sélectionnez Pré-actions dans la section Test. Cliquez sur (+) ajouter "Nouvelle action de script d'exécution". La commande xcrun simctl erase all peut être saisie directement sans nécessiter de script externe.

Options d'appel de 1. Code d'application pour réinitialiser l'application: 

A. Application UI. [UI Test] _ Fournissez un bouton de réinitialisation ou une autre action de l'interface utilisateur qui réinitialise l'application. L'élément d'interface utilisateur peut être exercé via XCUIApplication dans les routines XCTestsetUp(), tearDown() ou testSomething()

B. Paramètre de lancement. (Test d'interface utilisateur) Comme l'a noté Victor Ronin, un argument peut être transmis à partir du test setUp() ... 

class AppResetUITests: XCTestCase {

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

... à recevoir par le 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. [Test d'interface utilisateur, Test unitaire]} _ Sélectionnez le menu Produit> Schéma> Modifier le schéma…. Développez la section Scheme Run. (+) Ajoutez un paramètre comme MY_UI_TEST_MODE. Le paramètre sera disponible dans NSProcessInfo.processInfo().

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

Z. Appel direct. [Unit Test] Les ensembles de tests d'unités sont injectés dans l'application en cours d'exécution et peuvent appeler directement une routine myResetApplication() dans l'application. Avertissement: les tests unitaires par défaut sont exécutés après le chargement de l'écran principal. voir Test de la séquence de charge Cependant, les ensembles de tests d'interface utilisateur s'exécutent en tant que processus externe à l'application testée. Ainsi, ce qui fonctionne dans le test unitaire donne une erreur de liaison dans un test d'interface utilisateur.

class AppResetUnitTests: XCTestCase {

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

Mise à jour pour Swift 3.1/xcode 8.3

créer un en-tête de pontage dans la cible de test:

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

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

classe Springboard mise à jour

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()
      }
     }
    }
   }
14
JustinM

J'ai utilisé le @ ODManswer , mais je l'ai modifié pour fonctionner avec Swift 4. NB: certaines réponses S/O ne différencient pas les versions de Swift, qui présentent parfois des différences assez fondamentales. J'ai testé cela sur un simulateur iPhone 7 et un simulateur iPad Air en orientation portrait, et cela a fonctionné pour mon application.

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()
        }
    }
  }
}
9
DeeMickSee

Vous pouvez demander à votre application de se "nettoyer"

  • Vous utilisez XCUIApplication.launchArguments pour définir un indicateur
  • Dans AppDelegate, vous cochez

    if NSProcessInfo.processInfo (). arguments.contains ("YOUR_FLAG_NAME_HERE") { // nettoie ici }

9
Victor Ronin

J'ai utilisé @Chase Holland answer et mis à jour la classe Springboard en suivant la même approche pour réinitialiser le contenu et les paramètres à l'aide de l'application Paramètres. Ceci est utile lorsque vous devez réinitialiser les boîtes de dialogue d'autorisations.

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
odm

Pour iOS 11 sims up, j'ai fait une modification très légère pour appuyer sur l'icône "x" et pour sélectionner le correctif suggéré par @Code Monkey. La correction fonctionne bien sur les sims de téléphone 10.3 et 11.2. Pour mémoire, j'utilise Swift 3. Je pensais avoir copié et collé du code pour trouver le correctif un peu plus facilement. :)

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()
        }
    }
}
3
Craig Fisher

Cela semble fonctionner pour moi sur iOS 12.1 et simulateur

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)
    }
}
1
Peacemoon

En me basant sur les réponses de Chase Holland et d'odm, j'ai pu éviter le tap long et +3 offset bs en supprimant l'application dans des paramètres tels que dis:

import XCTest

class Springboard {
    static let springboard = XCUIApplication(bundleIdentifier: "com.Apple.springboard")
    static let settings = XCUIApplication(bundleIdentifier: "com.Apple.Preferences")
    static let isiPad = UIScreen.main.traitCollection.userInterfaceIdiom == .pad
    class func deleteApp(name: String) {
        XCUIApplication().terminate()
        if !springboard.icons[name].firstMatch.exists { return }
        settings.launch()
        goToRootSetting(settings)
        settings.tables.staticTexts["General"].tap()
        settings.tables.staticTexts[(isiPad ? "iPad" : "iPhone") + " Storage"].tap()
        while settings.tables.activityIndicators["In progress"].exists { sleep(1) }
        let appTableCellElementQuery = settings.tables.staticTexts.matching(identifier: name)
        appTableCellElementQuery.element(boundBy: appTableCellElementQuery.count - 1).tap()
        settings.tables.staticTexts["Delete App"].tap()
        isiPad ? settings.alerts.buttons["Delete App"].tap() : settings.buttons["Delete App"].tap()
        settings.terminate()
    }

    /**
     You may not want to do this cuz it makes you re-trust your computer and device.
     **/
    class func resetLocationAndPrivacySetting(passcode: String?) {
        settings.launch()
        goToRootSetting(settings)
        settings.tables.staticTexts["General"].tap()
        settings.tables.staticTexts["Reset"].tap()
        settings.tables.staticTexts["Reset Location & Privacy"].tap()

        passcode?.forEach({ char in
            settings.keys[String(char)].tap()
        })

        isiPad ? settings.alerts.buttons["Reset"].tap() : settings.buttons["Reset Settings"].tap()
    }

    class func goToRootSetting(_ settings: XCUIApplication) {
        let navBackButton = settings.navigationBars.buttons.element(boundBy: 0)
        while navBackButton.exists {
            navBackButton.tap()
        }
    }
}

Usage: 

Springboard.deleteApp(name: "AppName")
Springboard.resetLocationAndPrivacySetting()
1
bj97301

Mise à jour de Craig Fishers pour Swift 4. Mis à jour pour iPad en mode paysage, ne fonctionne probablement que pour le paysage à gauche.

importer XCTest

classe Tremplin {

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)
    }
}

}

0
Aaron

Voici une version Objective C des réponses ci-dessus pour supprimer une application et réinitialiser les avertissements (testés sur iOS 11 et 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];
}
0
tagy22