¿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:
-
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. -
Shell Script . Ejecute las pruebas desde un script de shell. Utilice
xcrun simctl erase all
oxcrun 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>
-
Acción de esquema de Xcode
.
Agregue
xcrun simctl erase all
(oxcrun 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 comandoxcrun 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)
}
}