swift xcode7 xctest xcode-ui-testing

¿Hay alguna manera de restablecer la aplicación entre pruebas en Swift XCTest UI?



xcode7 xcode-ui-testing (11)

¿Hay una llamada API dentro de XCTest que pueda poner en setUP () o tearDown () para restablecer la aplicación entre pruebas? Miré en la sintaxis de puntos de XCUIApplication y todo lo que vi fue el .launch ()

¿O hay una manera de llamar a un script de shell en Swift? Entonces podría llamar a xcrun entre métodos de prueba para restablecer el simulador.


Actualización de la respuesta de Craig Fishers para Swift 4. Actualizada para iPad en horizontal, probablemente solo funcione para la izquierda horizontal.

importar XCTest

trampolín de clase {

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

}


Actualizado para swift 3.1 / xcode 8.3

crear encabezado de puente en el objetivo de prueba:

#import <XCTest/XCUIApplication.h> #import <XCTest/XCUIElement.h> @interface XCUIApplication (Private) - (id)initPrivateWithPath:(NSString *)path bundleID:(NSString *)bundleID; - (void)resolve; @end

clase de trampolín actualizada

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


Aquí hay una versión del objetivo C de las respuestas anteriores para eliminar una aplicación y restablecer las advertencias (probado en iOS 11 y 12):

- (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]; }

..

- (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]; }


En este momento, la API pública en Xcode 7 y 8 y el Simulador no parece tener ningún método invocable desde las setUp() y tearDown() a "Restablecer contenido y configuración" para el simulador.

Existen otros enfoques posibles que utilizan API públicas:

  1. Código de solicitud Agregue un código de aplicación myResetApplication() para poner la aplicación en un estado conocido. Sin embargo, el control de estado del dispositivo (simulador) está limitado por el entorno limitado de la aplicación ... que no es de mucha ayuda fuera de la aplicación. Este enfoque está bien para borrar la persistencia controlable de la aplicación.

  2. Shell Script . Ejecute las pruebas desde un script de shell. Utilice xcrun simctl erase all o xcrun simctl uninstall <device> <app identifier> o similar entre cada ejecución de prueba para restablecer el simulador (o desinstalar la aplicación) . vea : "¿Cómo puedo restablecer el simulador de iOS desde la línea de comandos?"

macos> xcrun simctl --help # can uninstall a single application macos> xcrun simctl uninstall --help # Usage: simctl uninstall <device> <app identifier>

  1. Acción de esquema de Xcode . Agregue xcrun simctl erase all (o xcrun simctl erase <DEVICE_UUID> ) o similar a la sección Prueba de esquema. Seleccione el menú Producto> Esquema> Editar esquema ... Expanda la sección Prueba de esquema. Seleccione Pre-acciones en la sección Prueba. Haga clic en (+) y agregue "Nueva acción de ejecución de script". El comando xcrun simctl erase all se puede escribir directamente sin requerir ningún script externo.

Opciones para invocar 1. Código de aplicación para restablecer la aplicación:

A. UI de la aplicación . [Prueba de IU] Proporcione un botón de reinicio u otra acción de IU que restablezca la aplicación. El elemento UI se puede ejercer a través de la aplicación XCUIApplication en XCTest rutinas setUp() , tearDown() o testSomething() .

B. Parámetro de lanzamiento . [Prueba de IU] Como señaló Victor Ronin, se puede pasar un argumento desde la setUp() prueba setUp() ...

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

... para ser recibido por AppDelegate ...

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

C. Parámetro del esquema Xcode . [Prueba de IU, Prueba de unidad] Seleccione el menú Producto> Esquema> Editar esquema ... Expanda la sección Ejecutar esquema. (+) Agregue algún parámetro como MY_UI_TEST_MODE . El parámetro estará disponible en NSProcessInfo.processInfo() .

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

Z. Llamada directa . [Prueba unitaria] Los paquetes de prueba unitaria se inyectan en la aplicación en ejecución y pueden llamar directamente a alguna rutina myResetApplication() en la aplicación. Advertencia: las pruebas unitarias predeterminadas se ejecutan después de que se haya cargado la pantalla principal. consulte Secuencia de carga de prueba Sin embargo, los paquetes de prueba de IU se ejecutan como un proceso externo a la aplicación bajo prueba. Entonces, lo que funciona en la Prueba de Unidad da un error de enlace en una Prueba de IU.

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


Esto parece funcionar para mí en iOS 12.1 y simulador

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


Para iOS 11 sims up, hice una modificación muy leve para tocar el icono "x" y dónde tocamos según la corrección que sugirió @Code Monkey. Fix funciona bien en sims de teléfono 10.3 y 11.2. Para el registro, estoy usando swift 3. Pensé que había revisado algún código para copiar y pegar y encontrar la solución un poco más fácil. :)

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


Puede agregar una fase "Ejecutar secuencia de comandos" para crear fases en su objetivo de prueba para desinstalar la aplicación antes de ejecutar pruebas unitarias en su contra, desafortunadamente esto no ocurre entre los casos de prueba .

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

Actualizar

Entre pruebas, puede eliminar la aplicación a través del Springboard en la fase de lágrima. Aunque, esto requiere el uso de un encabezado privado de XCTest. (El volcado del encabezado está disponible en el WebDriverAgent de Facebook aquí ).

Aquí hay un código de muestra de una clase de Springboard para eliminar una aplicación de Springboard mediante tocar y mantener presionado:

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

Y entonces:

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

Los encabezados privados se importaron en el encabezado de puente Swift. Tendrá que importar:

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

Nota : A partir de Xcode 10, XCUIApplication(bundleIdentifier:) ahora expone XCUIApplication(bundleIdentifier:) , y los encabezados privados ya no son necesarios .


Puedes pedirle a tu aplicación que se "limpie"

  • Utiliza XCUIApplication.launchArguments para establecer algún indicador
  • En AppDelegate verificas

    if NSProcessInfo.processInfo (). argumentos.contains ("YOUR_FLAG_NAME_HERE") {// Haga una limpieza aquí}


Utilicé la answer @ ODM , pero la modifiqué para que funcione para Swift 4. Nota: algunas respuestas S / O no diferencian las versiones de Swift, que a veces tienen diferencias bastante fundamentales. He probado esto en un simulador de iPhone 7 y un simulador de iPad Air en orientación vertical, y funcionó para mi aplicación.

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


Utilicé la answer @Chase Holland y actualicé la clase Springboard siguiendo el mismo enfoque para restablecer el contenido y la configuración usando la aplicación Configuración. Esto es útil cuando necesita restablecer los cuadros de diálogo de permisos.

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


iOS 13 / Swift 5.1 eliminación basada en la interfaz de usuario

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