XCTest中是否存在API调用,我可以将其放入setUP()或tearDown()中以在两次测试之间重置应用程序?我查看了XCUIApplication的点语法,只看到了.launch()
或者有没有办法在Swift中调用Shell脚本?然后,我可以在测试方法之间调用xcrun来重置模拟器。
Answers:
您可以添加“运行脚本”阶段来在测试目标中构建阶段,以在针对应用运行单元测试之前卸载应用程序,但是不幸的是,这不在测试案例之间。
/usr/bin/xcrun simctl uninstall booted com.mycompany.bundleId
更新资料
在两次测试之间,您可以在tearDown阶段通过Springboard删除该应用程序。虽然,这确实需要使用XCTest的专用标头。(可从Facebook的WebDriverAgent此处获得标头转储。)
这是Springboard类中的一些示例代码,可通过点击并按住从Springboard中删除应用程序:
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()
}
}
}
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()
}
}
}
接着:
override func tearDown() {
Springboard.deleteMyApp()
super.tearDown()
}
专用标头已导入到Swift桥接标头中。您需要导入:
// Private headers from XCTest
#import "XCUIApplication.h"
#import "XCUIElement.h"
注意:从Xcode 10开始,XCUIApplication(bundleIdentifier:)
Apple现在公开了它,并且不再需要private标头。
NSBundle-bundleWithIdentifier/Path
,但是测试应用程序没有对应用程序包的引用。我的项目有许多目标,每个目标都有不同的名称,并且我希望能够在所有目标上使用Springboard类。
icon.buttons["DeleteButton"].tap()
在长按后运行而不是使用来点按CGVector
。
app.launch()
后Springboard.deleteMyApp()
:The request was denied by service delegate (SBMainWorkspace) for reason: NotFound ("Application "com.serpentisei.studyjapanese" is unknown to FrontBoard").
目前,Xcode和Simulator中的公共API并未显示可从该模拟器的“重置内容和设置”setUp()
及其tearDown()
XCText
子类中调用的任何方法。
还有其他使用公共API的方法:
应用代码。添加一些myResetApplication()
应用程序代码以使应用程序处于已知状态。但是,设备(模拟器)状态控制受到应用程序沙箱的限制……这在应用程序外部没有太大帮助。这种方法可以清除应用程序可控的持久性。
Shell脚本。从shell脚本运行测试。在每次测试运行之间使用xcrun simctl erase all
或xcrun simctl uninstall <device> <app identifier>
类似方法重置模拟器(或卸载应用程序)。请参见StackOverflow:“如何从命令行重置iOS模拟器?”
xcrun simctl --help
# Uninstall a single application
xcrun simctl uninstall --help
xcrun simctl uninstall <device> <app identifier>
# Erase a device's contents and settings.
xcrun simctl erase <device>
xcrun simctl erase all # all existing devices
# Grant, revoke, or reset privacy and permissions
simctl privacy <device> <action> <service> [<bundle identifier>]
xcrun simctl erase all
(或xcrun simctl erase <DEVICE_UUID>
)或类似命令。选择产品>方案>编辑方案...菜单。展开方案测试部分。在“测试”部分下选择“预操作”。单击(+)添加“新建运行脚本操作”。xcrun simctl erase all
可以直接键入命令,而无需任何外部脚本。调用选项1.重置应用程序的应用程序代码:
A.应用程序界面。[UI测试]提供一个重置按钮或其他UI动作来重置应用程序。UI元素可通过行使XCUIApplication
在XCTest
例程setUp()
,tearDown()
或testSomething()
。
B.启动参数。[UI测试]如Victor Ronin所述,可以从测试中传递参数setUp()
。
class AppResetUITests: XCTestCase {
override func setUp() {
// ...
let app = XCUIApplication()
app.launchArguments = ["MY_UI_TEST_MODE"]
app.launch()
...由AppDelegate
...接收
class AppDelegate: UIResponder, UIApplicationDelegate {
func application( …didFinishLaunchingWithOptions… ) -> Bool {
// ...
let args = NSProcessInfo.processInfo().arguments
if args.contains("MY_UI_TEST_MODE") {
myResetApplication()
}
C. Xcode方案参数。[UI测试,单元测试] 选择产品>方案>编辑方案…菜单。展开“方案运行”部分。(+)添加一些参数,例如MY_UI_TEST_MODE
。该参数将在中可用NSProcessInfo.processInfo()
。
// ... in application
let args = NSProcessInfo.processInfo().arguments
if args.contains("MY_UI_TEST_MODE") {
myResetApplication()
}
Z.直接通话。[单元测试]单元测试包被注入到正在运行的应用程序中,并且可以直接myResetApplication()
在应用程序中调用某些例程。注意:加载主屏幕后,将运行默认的单元测试。请参阅测试加载顺序。但是,UI测试包作为被测试应用程序外部的进程运行。因此,在单元测试中有效的方法在UI测试中给出了链接错误。
class AppResetUnitTests: XCTestCase {
override func setUp() {
// ... Unit Test: runs. UI Test: link error.
myResetApplication() // visible code implemented in application
xcrun simctl erase all
是一个很好的建议-谢谢!
更新为Swift 3.1 / Xcode 8.3
在测试目标中创建桥接头:
#import <XCTest/XCUIApplication.h>
#import <XCTest/XCUIElement.h>
@interface XCUIApplication (Private)
- (id)initPrivateWithPath:(NSString *)path bundleID:(NSString *)bundleID;
- (void)resolve;
@end
更新了Springboard类
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()
}
}
}
}
您可以要求您的应用自行“清理”
你XCUIApplication.launchArguments
用来设置一些标志
在AppDelegate中检查
如果NSProcessInfo.processInfo()。arguments.contains(“ YOUR_FLAG_NAME_HERE”){//在这里进行清理}
我使用了@ ODM 答案,但对其进行了修改,使其可用于Swift 4。我已经在iPhone 7模拟器和iPad Air模拟器上以纵向方向对其进行了测试,它适用于我的应用程序。
斯威夫特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()
}
}
}
}
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()
}
}
}
我使用@Chase Holland答案,并按照相同的方法使用“设置”应用程序重置内容和设置来更新Springboard类。当您需要重置权限对话框时,这很有用。
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()
}
}
}
}
XCUIApplication(privateWithPath: …)
不会在Swift 3中公开吗?
我看到了很多有关卸载您的应用程序setUp
或tearDown
测试的答案。
但是,您可以在测试之前通过在测试目标中添加运行脚本阶段来轻松卸载应用程序。
为此:
然后,用# Type a script or drag a script file from your workspace to insert its path.
以下命令替换占位符:
xcrun simctl boot ${TARGET_DEVICE_IDENTIFIER}
xcrun simctl uninstall ${TARGET_DEVICE_IDENTIFIER} YOUR_APP_BUNDLE
xcrun simctl create
然后在这些模拟器上启动测试,并将多个目标设置为xcodebuild
test
command。如果它不工作,尝试选择-only-testing:
的xcodebuild test-without-building
分离UITests自己。
从Xcode 11.4开始,如果您只想重置权限,则可以resetAuthorizationStatus(for:)
在的实例上使用XCUIApplication
,请参阅
https://developer.apple.com/documentation/xctest/xcuiapplication/3526066-resetauthorizationstatusforresou
您也可以simctl
根据需要使用Xcode 11.4发行说明中引用的内容:
simctl现在支持修改隐私权限。您可以修改隐私权限以创建已知状态以进行测试。例如,要允许示例应用程序访问照片库而没有任何提示:
xcrun simctl privacy <device> grant photos com.example.app
要将所有权限重置为默认值,就像以前从未安装过该应用程序一样:
xcrun simctl privacy <device> reset all com.example.app
。
对于iOS 11 sims up,我进行了非常细微的修改,以点按“ x”图标,然后按@Code Monkey建议的修复方式点按。Fix在10.3和11.2手机SIM卡上均可正常运行。作为记录,我使用的是swift3。我想通过那里的一些代码来复制和粘贴以找到更容易的修复程序。:)
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()
}
}
}
这似乎对我在iOS 12.1和模拟器上有效
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)
}
}
iOS 13.1 / Swift 5.1基于UI的删除
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)
}
更新Craig Fishers的Swift 4答案。针对iPad在景观中进行了更新,可能仅适用于左侧景观。
导入XCTest
跳板类{
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)
}
}
}
这是上述答案的Objective C版本,用于删除应用并重置警告(已在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];
}
这适用于所有操作系统版本(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()
}
}
gitlab-ci.yml
文件的优雅解决方案解决了该问题。