web-dev-qa-db-ja.com

Swift XCTest UIのテスト間でアプリをリセットする方法はありますか?

XCTest内に、テストの間にアプリをリセットするためにsetUP()またはtearDown()に入れることができるAPI呼び出しがありますか? XCUIApplicationのドット構文を見て、見たのは.launch()だけでした

または、Swiftでシェルスクリプトを呼び出す方法はありますか?次に、xcrunをテストメソッドの中間で呼び出して、シミュレータをリセットできます。

61
Laser Hawk

「スクリプトの実行」フェーズを追加して、テストターゲットにビルドフェーズを作成し、単体テストを実行する前にアプリをアンインストールして、 残念ながら、これはテストケースの間ではありませんが、

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

更新


テスト間で、tearDownフェーズでSpringboardを介してappを削除できます。ただし、これにはXCTestのプライベートヘッダーを使用する必要があります。 (ヘッダーダンプは FacebookのWebDriverAgentはこちら から入手できます。)

以下は、タップアンドホールドでSpringboardからアプリを削除するSpringboardクラスのサンプルコードです。

スウィフト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()
        }
    }
 }

その後:

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

プライベートヘッダーは、Swiftブリッジングヘッダーにインポートされました。インポートする必要があります:

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

:Xcode 10の時点で、XCUIApplication(bundleIdentifier:)はAppleによって公開され、プライベートヘッダーはでなくなりました必要です

69
Chase Holland

現時点では、Xcode 7および8のpublic APIおよびシミュレーターには、setUp()およびtearDown()XCTextサブクラスから呼び出し可能なメソッドがありません。シミュレーターの「内容と設定をリセットする」。

パブリックAPIを使用する他の可能なアプローチがあります。

  1. アプリケーションコードmyResetApplication()アプリケーションコードを追加して、アプリケーションを既知の状態にします。ただし、デバイス(シミュレーター)の状態制御は、アプリケーションサンドボックスによって制限されます...これは、アプリケーションの外部ではあまり役に立ちません。このアプローチは、アプリケーションで制御可能な永続性をクリアするのに適しています。

  2. シェルスクリプト。シェルスクリプトからテストを実行します。各テスト実行の間にxcrun simctl erase allまたはxcrun simctl uninstall <device> <app identifier>などを使用して、シミュレーターをリセットします(またはアプリをアンインストールします)StackOverflow:「コマンドラインからiOSシミュレータをリセットするにはどうすればよいですか?」を参照

macos> xcrun simctl --help
# can uninstall a single application
macos> xcrun simctl uninstall --help  
# Usage: simctl uninstall <device> <app identifier>
  1. Xcodeスキーマアクションxcrun simctl erase all(またはxcrun simctl erase <DEVICE_UUID>)または同様のスキームテストセクションを追加します。 [製品]> [スキーム]> [スキームの編集…]メニューを選択します。 [スキームテスト]セクションを展開します。 [テスト]セクションで[事前アクション]を選択します。 [+]をクリックして、[新しいスクリプトの実行アクション]を追加します。コマンドxcrun simctl erase allは、外部スクリプトを必要とせずに直接入力できます。

呼び出すためのオプション1。アプリケーションをリセットするアプリケーションコード

A.アプリケーションUI[UI Test]アプリケーションをリセットするリセットボタンまたはその他のUIアクションを提供します。 UI要素は、XCUIApplicationルーチンsetUp()tearDown()、またはtestSomething()XCTestを介して実行できます。

B.パラメータの起動[UI Test]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
36
l --marc l

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

@ ODManswer を使用しましたが、Swift 4.で機能するように変更しました。注:一部のS/O回答では、 Swiftバージョン。かなり基本的な違いがある場合があります。これをiPhone 7シミュレーターとiPad Airシミュレーターで縦向きでテストしましたが、アプリで機能しました。

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

アプリに「クリーンアップ」を依頼することができます

  • XCUIApplication.launchArgumentsを使用してフラグを設定します
  • AppDelegateで確認します

    if NSProcessInfo.processInfo()。arguments.contains( "YOUR_FLAG_NAME_HERE"){//ここでクリーンアップを行います}

9
Victor Ronin

@Chase Holland answer を使用し、同じアプローチに従って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()
            }
        }
    }
}
7
odm

IOS 11のシムアップの場合、「x」アイコンをタップするために、そして@Code Monkeyが提案した修正ごとにタップするように、わずかな変更を加えました。修正は10.3と11.2の両方の電話シムでうまく機能します。記録のために、私はSwift 3を使用しています。修正を見つけるのに少し簡単にコピーして貼り付けるためにそこにあるいくつかのコードを調べたと思います。 :)

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

これは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)
    }
}
2
Peacemoon

Chase Hollandとodmの回答に基づいて、disなどの設定でアプリを削除することで、ロングタップと+3オフセットbsを回避できました。

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

使用法:

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

SwiftのCraig Fishers回答を更新します。4.横向きのiPad用に更新されました。おそらく左向きの横向きの場合のみ機能します。

xCTestをインポートする

クラス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)
    }
}

}

0
Aaron

アプリを削除して警告をリセットする上記の回答の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];
}
0
tagy22