iOS 自動(dòng)化測(cè)試(XCTest, UITests)

文章目錄

  • 前言
  • 如何創(chuàng)建自動(dòng)化測(cè)試
  • 創(chuàng)建好的自動(dòng)化測(cè)試在哪里?
  • 如何使用自動(dòng)化測(cè)試
  • 生命周期(運(yùn)行流程)
  • 具體使用介紹
  • 初始化 App
  • 獲取元素
  • 根據(jù)類型取元素
  • 根據(jù) label 取元素
  • 根據(jù)下標(biāo)取元素
  • 根據(jù) identifier 取元素
  • 對(duì)元素的操作
  • 點(diǎn)擊
  • 雙擊
  • 長(zhǎng)按
  • 滑動(dòng)
  • 捏合
  • 旋轉(zhuǎn)
  • 實(shí)際用例
  • 具體點(diǎn)擊 UITableView 某一行
  • 直接打開(kāi)其他 App
  • 系統(tǒng)桌面 App(springboard)
  • 申請(qǐng)系統(tǒng)權(quán)限, 點(diǎn)擊系統(tǒng)權(quán)限彈框(例如通知權(quán)限)
  • Home鍵
  • 系統(tǒng)音量按鍵
  • Siri
  • 設(shè)備轉(zhuǎn)方向
  • 項(xiàng)目地址

前言

最近正在學(xué)習(xí) iOS 自動(dòng)化測(cè)試(系統(tǒng)自帶的 XCTest), 就總結(jié)一下自動(dòng)化測(cè)試一些知識(shí).
內(nèi)容可能有點(diǎn)啰嗦, 如想直接看代碼, 可直接跳到最后一欄, 去下載項(xiàng)目

如何創(chuàng)建自動(dòng)化測(cè)試

這里分兩種創(chuàng)建情況

1.剛新建項(xiàng)目
創(chuàng)建項(xiàng)目時(shí), 勾上 Include UI Tests 即可


image.png

2.已有了項(xiàng)目,卻沒(méi)有自動(dòng)化測(cè)試
其實(shí)就是添加 target
我們點(diǎn)擊 ‘+’ 號(hào),


image.png

選擇 UI Testing Bundle


image.png

創(chuàng)建好的自動(dòng)化測(cè)試在哪里?

創(chuàng)建好之后, 是在該目錄下(看圖), 默認(rèn)名稱是 項(xiàng)目名 + UITests
看到這里, 寫(xiě)過(guò) app extension 的老哥應(yīng)該都明白了, 其實(shí)這個(gè)自動(dòng)化測(cè)試就是一個(gè) app extension 來(lái)的, 我們可以隨意刪除和創(chuàng)建, 甚至創(chuàng)建多個(gè)都是沒(méi)問(wèn)題的


image.png

如何使用自動(dòng)化測(cè)試

這里一般有兩種使用法

1.在 Show the Test Navigator 中使用
如圖, 放鼠標(biāo)到, 放在函數(shù)上, 例如testExample()
這時(shí)我們就能看到右邊有個(gè)播放的小箭頭, 點(diǎn)擊這個(gè)小箭頭, 我們就開(kāi)始運(yùn)行這個(gè)testExample()函數(shù)了
如果運(yùn)行大寫(xiě) T(XQUITestDemoUITests), 就是運(yùn)行這個(gè)測(cè)試模塊中所有函數(shù)的意思
運(yùn)行自動(dòng)化測(cè)試, 和運(yùn)行項(xiàng)目一樣的, 可以先選擇設(shè)備, 就是選擇真機(jī)或者某個(gè)模擬器

剛創(chuàng)建時(shí), 只有 testLaunchPerformance()testExample() 兩個(gè)函數(shù)
其他函數(shù)是我后面寫(xiě)的

image.png

2.在 UITest 模塊的 .swift 文件里面選擇運(yùn)行
上面的 testExample() 這些函數(shù), 其實(shí)就是關(guān)聯(lián)這個(gè)文件里面的函數(shù)
我們點(diǎn)開(kāi) UITest 模塊的 .swift 文件(如下圖), 就一切明了了

image.png

代碼行數(shù)那里的四邊形, 點(diǎn)擊效果和上面說(shuō)的一樣, 就是運(yùn)行自動(dòng)化測(cè)試
點(diǎn)擊 class 旁邊的運(yùn)行, 是運(yùn)行整個(gè)類里面所有函數(shù)的(和上面說(shuō)的運(yùn)行整個(gè)測(cè)試模塊一樣)

注意! 能單獨(dú)執(zhí)行的函數(shù)一定要 test 開(kāi)頭(就是旁邊有四邊形的函數(shù))
就比如 testA() 這個(gè)就是可以的. methodA() 旁邊就沒(méi)有四邊形.

生命周期(運(yùn)行流程)

周期如下

1.setUp()
2.自定義執(zhí)行的函數(shù)
3.tearDown()
這里有個(gè)點(diǎn), 要注意一下, 比如你當(dāng)前測(cè)試模塊里面有 testA(), testB() 兩個(gè)函數(shù).
然后你直接點(diǎn)擊運(yùn)行整個(gè)測(cè)試模塊, 他執(zhí)行的順序是.

1.setUp()
2.testA()
3.tearDown()
然后再執(zhí)行

1.setUp()
2.testB()
3.tearDown()
就是你會(huì)看到APP啟動(dòng)和關(guān)閉了兩次.

具體使用介紹

初始化 App

// 初始化 XCUIApplication
let app = XCUIApplication()
// 啟動(dòng)app 
app.launch()

// 默認(rèn)不填 bundleIdentifier, 就會(huì)初始化當(dāng)前項(xiàng)目APP
// 如果是想搞其他APP, 可傳入 bundleIdentifier 初始化, 就可獲得其實(shí)例
let sefariApp = XCUIApplication.init(bundleIdentifier: "com.apple.mobilesafari")

注意, 以下文章出現(xiàn) app 的代碼, 都是指代 let app = XCUIApplication() 這個(gè)

獲取元素

這里我就舉例幾種常用的就行, 其他的, 大家可自行研究

根據(jù)類型取元素

可直接查看系統(tǒng) XCUIElementTypeQueryProvider

image.png

比如這樣就能取得該 app 下面所有的 button

app.buttons

但取到 XCUIElementQuery, 還不能直接用
這個(gè)時(shí)候我們?nèi)〉骄唧w某個(gè)元素才行

為了比較好明白, 這里我舉一個(gè)例子, UI是這樣


image.png

以下是 print(app.debugDescription) 輸出的數(shù)據(jù)
這里再啰嗦一下, debugDescription 屬性對(duì)于獲取元素層級(jí)來(lái)說(shuō)挺舒服的, 我們要記得常用 debugDescription

 →Application, 0x2819e7720, pid: 4843, label: 'XQUITestDemo'
    Window (Main), 0x2819e78e0, {{0.0, 0.0}, {375.0, 667.0}}
      Other, 0x2819e79c0, {{0.0, 0.0}, {375.0, 667.0}}
        Other, 0x2819e7aa0, {{0.0, 0.0}, {375.0, 667.0}}
          Other, 0x2819e7b80, {{0.0, 0.0}, {375.0, 667.0}}
            Other, 0x2819e7c60, {{0.0, 0.0}, {375.0, 667.0}}
              Other, 0x2819e7d40, {{0.0, 0.0}, {375.0, 667.0}}
                Other, 0x2819e7e20, {{0.0, 0.0}, {375.0, 667.0}}
                  NavigationBar, 0x2819e7f00, {{0.0, 20.0}, {375.0, 44.0}}, identifier: '我的'
                    StaticText, 0x2819e8000, {{170.0, 32.0}, {35.0, 20.5}}, label: '我的'
                  Other, 0x2819e80e0, {{0.0, 0.0}, {375.0, 667.0}}
                    Other, 0x2819e81c0, {{0.0, 0.0}, {375.0, 667.0}}
                      Other, 0x2819e82a0, {{0.0, 64.0}, {375.0, 603.0}}
                        Button, 0x280c8eae0, {{30.0, 214.0}, {60.0, 60.0}}, identifier: 'touchMe', label: '點(diǎn)我'
                          StaticText, 0x2819e8460, {{41.5, 233.0}, {37.0, 22.0}}, label: '點(diǎn)我'
            TabBar, 0x2819e8540, {{0.0, 618.0}, {375.0, 49.0}}
              Button, 0x2819e8620, {{2.0, 619.0}, {184.0, 48.0}}, label: '首頁(yè)'
              Button, 0x2819e1b20, {{190.0, 619.0}, {183.0, 48.0}}, label: '我的', Selected
      Other, 0x2819e1a40, {{0.0, 0.0}, {375.0, 667.0}}, Disabled
        Other, 0x2819e0b60, {{0.0, 0.0}, {375.0, 667.0}}, identifier: 'SVProgressHUD'
    Window, 0x2819e1ce0, {{0.0, 0.0}, {375.0, 667.0}}
      Other, 0x2819e1f80, {{0.0, 0.0}, {375.0, 667.0}}
        Other, 0x2819e2060, {{0.0, 0.0}, {375.0, 667.0}}

注意, Other 對(duì)應(yīng)的是 UIView

我們現(xiàn)在要點(diǎn)擊 tabbar 首頁(yè)這個(gè)按鈕, 跳轉(zhuǎn)到首頁(yè)
那么該怎么做呢? 很簡(jiǎn)單, 代碼就兩句

// 取元素
let homePageBtn = app.tabBars.buttons["首頁(yè)"]
// 點(diǎn)擊元素
homePageBtn.tap()

這里我解析一下第一句取元素

app.tabBars 取到的是以下數(shù)據(jù)

TabBar, 0x2835c3800, {{0.0, 618.0}, {375.0, 49.0}}
    Button, 0x2835c38e0, {{2.0, 619.0}, {184.0, 48.0}}, label: '首頁(yè)'
    Button, 0x2835c39c0, {{190.0, 619.0}, {183.0, 48.0}}, label: '我的', Selected

然后 app.tabBars.buttons 取到的是

Button, 0x2835c38e0, {{2.0, 619.0}, {184.0, 48.0}}, label: '首頁(yè)'
Button, 0x2835c39c0, {{190.0, 619.0}, {183.0, 48.0}}, label: '我的', Selected

這個(gè)時(shí)候, 我們根據(jù)類型取元素, 就已經(jīng)取到最后了
想取首頁(yè)按鈕的話, 有幾種方法, 請(qǐng)繼續(xù)往下看

根據(jù) label 取元素

根據(jù) label 來(lái)獲取 button

app.tabBars.buttons["首頁(yè)"]

當(dāng)然, 按照當(dāng)前這個(gè)UI, 最簡(jiǎn)單就是 app.buttons[“首頁(yè)”]
但是為了能夠更準(zhǔn)確的取到某個(gè)元素, 最好不要吝嗇一點(diǎn)代碼

根據(jù)下標(biāo)取元素

因?yàn)榇蛴〉臄?shù)據(jù), 里面的元素是有序的. 那么我們也可以通過(guò)下標(biāo)來(lái)獲取元素

// 傳入具體的下標(biāo), 取出按鈕
app.tabBars.buttons.element(boundBy: 0)

// 當(dāng)然, 也有類似數(shù)組一樣取法, 取第一個(gè)
app.windows.tabBars.firstMatch

根據(jù) identifier 取元素

不過(guò)你要提前在項(xiàng)目的代碼, 或者 xib 中設(shè)置好 identifier 才行

// 這里取的不是 tabbar 的 首頁(yè)按鈕, 是那個(gè)黃色按鈕
app.tabBars.element(matching: .button, identifier: "touchMe")
  • 在代碼中設(shè)置
    accessibilityIdentifier 就是設(shè)置自動(dòng)化測(cè)試時(shí)的 identifier
let btn = UIButton()
btn.frame = CGRect.init(x: 30, y: 150, width: 60, height: 60)
btn.setTitle("點(diǎn)我", for: .normal)
btn.backgroundColor = UIColor.orange
btn.accessibilityIdentifier = "touchMe"
  • xib 或者 storyboard 中設(shè)置


    image.png

對(duì)元素的操作

以下 button 就代表是一個(gè)按鈕元素( XCUIElement類 )

點(diǎn)擊
button.tap()
雙擊
button.doubleTap()
長(zhǎng)按
// 長(zhǎng)按三秒
button.press(forDuration: 3)
滑動(dòng)
// 上掃
button.swipeUp()

// 下掃
button.swipeDown()

// 左掃
button.swipeLeft()

// 右掃
button.swipeRight()

捏合
button.pinch(withScale: 1.5, velocity: 1)
旋轉(zhuǎn)
button.rotate(0.5, withVelocity: 1)

實(shí)際用例

這里舉例一些我學(xué)習(xí)的時(shí)候, 搜了挺久, 都沒(méi)搜到的實(shí)際用例吧
這些用例我都放在了 項(xiàng)目 里面,有興趣的,可直接去下載 項(xiàng)目 運(yùn)行一下

具體點(diǎn)擊 UITableView 某一行

示例 UI 如下


image.png

打印 app.debugDescription 數(shù)據(jù)如下

 →Application, 0x2838da060, pid: 1007, label: 'XQUITestDemo'
    Window (Main), 0x2838db560, {{0.0, 0.0}, {375.0, 667.0}}
      Other, 0x2838db640, {{0.0, 0.0}, {375.0, 667.0}}
        Other, 0x2838db720, {{0.0, 0.0}, {375.0, 667.0}}
          Other, 0x2838db800, {{0.0, 0.0}, {375.0, 667.0}}
            Other, 0x2838db8e0, {{0.0, 0.0}, {375.0, 667.0}}
              Other, 0x2838db9c0, {{0.0, 0.0}, {375.0, 667.0}}
                Other, 0x2838dbaa0, {{0.0, 0.0}, {375.0, 667.0}}
                  NavigationBar, 0x2838dbb80, {{0.0, 20.0}, {375.0, 44.0}}, identifier: 'TableView'
                    Button, 0x2838dbc60, {{0.0, 20.0}, {62.0, 44.0}}, label: '首頁(yè)'
                    StaticText, 0x2838dbd40, {{147.0, 32.0}, {81.0, 20.5}}, label: 'TableView'
                  Other, 0x2838dbe20, {{0.0, 0.0}, {375.0, 667.0}}
                    Other, 0x2838dbf00, {{0.0, 0.0}, {375.0, 667.0}}
                      Other, 0x2838de760, {{0.0, 64.0}, {375.0, 603.0}}
                        Table, 0x2838debc0, {{0.0, 64.0}, {375.0, 611.0}}
                          Cell, 0x2838dea00, {{0.0, 64.0}, {375.0, 43.5}}
                            Image, 0x2838d0000, {{15.0, 73.5}, {24.0, 24.0}}
                            StaticText, 0x2838d00e0, {{54.0, 64.0}, {306.0, 43.5}}, label: '測(cè)試: 0'
                            Other, 0x2838d01c0, {{54.0, 107.0}, {321.0, 0.5}}
                            Button, 0x2838d02a0, {{281.0, 69.0}, {74.0, 34.0}}, label: '我是按鈕'
                              StaticText, 0x2838d0380, {{281.0, 75.0}, {74.0, 22.0}}, label: '我是按鈕'
                          Cell, 0x2838d0460, {{0.0, 107.5}, {375.0, 43.5}}
                            Image, 0x2838d0540, {{15.0, 117.0}, {24.0, 24.0}}
                            StaticText, 0x2838d0620, {{54.0, 107.5}, {306.0, 43.5}}, label: '測(cè)試: 1'
                            Other, 0x2838d0700, {{54.0, 150.5}, {321.0, 0.5}}
                            Button, 0x2838d07e0, {{281.0, 112.5}, {74.0, 34.0}}, label: '我是按鈕'
                              StaticText, 0x2838d08c0, {{281.0, 118.5}, {74.0, 22.0}}, label: '我是按鈕'
                          ...后面 cell 就省略了, 都是一樣的數(shù)據(jù)格式
            TabBar, 0x2838f0fc0, {{0.0, 618.0}, {375.0, 49.0}}
              Button, 0x2838f10a0, {{2.0, 619.0}, {184.0, 48.0}}, label: '首頁(yè)', Selected
              Button, 0x2838f1180, {{190.0, 619.0}, {183.0, 48.0}}, label: '我的'

其實(shí)邏輯挺簡(jiǎn)單, 就是找出 cell, 然后并且點(diǎn)擊而已
其實(shí)最關(guān)鍵的字段是isHittable, 請(qǐng)看以下代碼, 雖然有點(diǎn)長(zhǎng). 不過(guò)請(qǐng)耐心看完

class XQUITestDemoUITests: XCTestCase {

  /// 測(cè)試 tableView
  func testTableView() {

    // 初始化, 并打開(kāi)APP
    let app = XCUIApplication()
    app.launch()

    // 讀取 tableView 元素
    let tables = app.tables.firstMatch

    // 獲取下標(biāo) 30 的 cell
    let cell = tables.cells.element(boundBy: 30)
    // 調(diào)用封裝的方法, 滾動(dòng)到該 cell
    if tables.xq_scrollToElement(element: cell) {
      // 已經(jīng)找到 cell, 點(diǎn)擊 cell
      cell.tap()
    }

  }

}


/// 對(duì)于 tableview 的封裝
extension XCUIElement {
    /// 滾動(dòng)到某個(gè)元素
    /// 默認(rèn)向下滾動(dòng)
    /// 這里可以再封裝一下的,比如可以向上滾動(dòng), 可以無(wú)限循環(huán)上下滾動(dòng)等等...
    /// - Parameter element: UI元素
    /// - Parameter isAutoStop: true 滾動(dòng)到最后一個(gè), 自動(dòng)停下來(lái)
    ///
    /// 返回 true 表示找到了傳入的元素
    ///
    func xq_scrollToElement(element: XCUIElement, isAutoStop: Bool = true) -> Bool {

        // 判斷是否是, tableView
        if self.elementType != .table {
            return false
        }

        // 一直滾動(dòng)到某個(gè)元素可被點(diǎn)擊為止
        while !element.isHittable {
            
            // 滾動(dòng)到最后就停下來(lái)
            if isAutoStop {
              // 獲取最后一個(gè)元素
                let lastElement = self.cells.element(boundBy: self.cells.count - 1)
                // 滾動(dòng)到最后了, 那么就停下來(lái)
                if lastElement.isHittable {
                    return false
                }
            }
            
            self.swipeUp()
        }

        return true
    }
}

直接打開(kāi)其他 App

這里舉例打開(kāi) Safari.

// 傳入 bundle id, 初始化某個(gè) app
let safariApp = XCUIApplication.init(bundleIdentifier: "com.apple.mobilesafari")
safariApp.launch()

當(dāng)然, 我們打開(kāi) Safari 之后, 也能取到 safari 上面的元素, 并且能操作

系統(tǒng)桌面 App(springboard)

如果我們想去獲取當(dāng)前狀態(tài)欄上面的信息. 比如電量, 是否正常充電, 信號(hào)強(qiáng)度這些的
其實(shí)可以通過(guò)初始化桌面APP,來(lái)獲取的

// 注意, 這里不用 launch() 了 
let springboard = XCUIApplication.init(bundleIdentifier: "com.apple.springboard")
// 第一次獲取桌面元素信息, 有時(shí)候會(huì)特別慢...所以這里并不是卡死了, 請(qǐng)耐心等待
// 不知道其中緣由,感覺(jué)有點(diǎn)玄學(xué)
// 反正我測(cè)的時(shí)候一般要等待 3 ~ 20 秒
print(safariApp.debugDescription)

當(dāng)然,我們可以調(diào)用 Home 鍵, 回到桌面, 然后根據(jù)獲取的信息, 去點(diǎn)擊桌面APP,這樣也可以行得通
這個(gè) springboard 可以搞很多騷操作, 具體可看我項(xiàng)目, 里面有一些實(shí)際用例

申請(qǐng)系統(tǒng)權(quán)限, 點(diǎn)擊系統(tǒng)權(quán)限彈框(例如通知權(quán)限)

代碼如下

class XQUITestDemoUITests: XCTestCase {

    /// 測(cè)試系統(tǒng)按鈕自動(dòng)點(diǎn)擊, 通知權(quán)限
    func testSystemAlertNotification() {

      // 初始化, 并打開(kāi)APP
      let app = XCUIApplication()
      app.launch()
        
        // 點(diǎn)擊 app 里面的 cell, 去申請(qǐng)通知權(quán)限
      let view = app.windows.cells.element(boundBy: 9)
      view.tap()
        
        // 調(diào)用封裝好的方法, 點(diǎn)擊下標(biāo) 1 的系統(tǒng) Alert 按鈕
        // 下標(biāo) 1, 就是右邊同意按鈕
      self.xq_tapSystemAlert(index: 1)
        
        // 等待一會(huì)
      let _ = app.wait(for: .notRunning, timeout: 3)
    }

}
    


extension XCTestCase {

    /// 點(diǎn)擊系統(tǒng)彈框
    /// - Parameter index: 按鈕的下標(biāo).
    /// 下標(biāo)是從左邊開(kāi)始算起, 0為起始下標(biāo).   就比如通知權(quán)限, 要同意的話, 就傳入 1
    func xq_tapSystemAlert(index: Int) {
        let springboard = XCUIApplication.init(bundleIdentifier: "com.apple.springboard")
        springboard.xq_tapAlert(index: index)
    }
    
}

extension XCUIApplication {
    
    ///
    /// 注意, actionSheet 的彈框是沒(méi)辦法調(diào)用這個(gè)點(diǎn)擊的.
    /// 因?yàn)?actionSheet 是用兩個(gè) ScrollView 組成...并且系統(tǒng)不認(rèn)為他是一個(gè) alert...
    ///
    
    /// 點(diǎn)擊彈框
    /// - Parameter index: 按鈕的下標(biāo).
    /// 下標(biāo)是從左邊開(kāi)始算起, 0為起始下標(biāo).
    func xq_tapAlert(index: Int) {
        let alerts = self.windows.alerts
        if alerts.count > 0 {
            let _ = self.wait(for: .notRunning, timeout: 1)
            alerts.buttons.element(boundBy: index).tap()
            let _ = self.wait(for: .notRunning, timeout: 1)
        }
    }
    
}

Home鍵

當(dāng)前沒(méi)有發(fā)現(xiàn)能雙擊 Home 鍵的方法, 有知道的老哥, 請(qǐng)留言告訴我

// 單擊 Home 鍵
XCUIDevice.shared.press(.home)
系統(tǒng)音量按鍵
// 調(diào)節(jié)音量, +
XCUIDevice.shared.press(.volumeUp)

// 調(diào)節(jié)音量, -
XCUIDevice.shared.press(.volumeDown)
Siri

突然喚醒 Siri, 會(huì)說(shuō)話很大聲, 在公司玩耍的話, 建議先調(diào)小聲 ??

// 喚醒 Siri, 并輸入語(yǔ)句
XCUIDevice.shared.siriService.activate(voiceRecognitionText: "我?guī)浢?");
設(shè)備轉(zhuǎn)方向

注意, 手機(jī)要先允許轉(zhuǎn)向才行

// 調(diào)節(jié)方向
XCUIDevice.shared.orientation = .landscapeLeft
項(xiàng)目地址

不知不覺(jué), 寫(xiě)了那么多…有點(diǎn)啰嗦了??
這里給上 [項(xiàng)目](https://github.com/SyKingW/XQUITestDemo
地址, 想看代碼的, 就去下載吧

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容