文章目錄
- 前言
- 如何創(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 即可

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

選擇 UI Testing Bundle

創(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)題的

如何使用自動(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ě)的

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

代碼行數(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

比如這樣就能取得該 app 下面所有的 button
app.buttons
但取到 XCUIElementQuery, 還不能直接用
這個(gè)時(shí)候我們?nèi)〉骄唧w某個(gè)元素才行
為了比較好明白, 這里我舉一個(gè)例子, UI是這樣

以下是 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 如下

打印 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
地址, 想看代碼的, 就去下載吧
