穩(wěn)定性測(cè)試:測(cè)試應(yīng)用程序在長(zhǎng)時(shí)間運(yùn)行過(guò)程中是否存在內(nèi)存泄漏访诱、崩潰等問(wèn)題拒贱,以確保應(yīng)用程序具有較高的穩(wěn)定性和可靠性铐刘。
對(duì)于安卓端,官方提供了很好的穩(wěn)定性測(cè)試工具:monkey镰惦。 相比較而言迷守,iOS則沒(méi)有,而且當(dāng)前網(wǎng)絡(luò)上似乎也沒(méi)有很好的第三方工具可以使用旺入,因此只能自己寫(xiě)了兑凿。
我們要開(kāi)發(fā)的iOS穩(wěn)定性測(cè)試程序,應(yīng)該至少包含以下內(nèi)容:
- 持續(xù)隨機(jī)觸發(fā)UI事件
- 崩潰重啟茵瘾,測(cè)試不中斷
- 日志記錄
首先我們確定以上設(shè)想的可行性礼华,然后再制定實(shí)施方案。在iOS原生開(kāi)發(fā)語(yǔ)言swift和object-C中提供了可進(jìn)行單元測(cè)試和UI測(cè)試的XCTest框架拗秘,而同樣可進(jìn)行移動(dòng)端UI測(cè)試的第三方框架還有Appium等圣絮,但相比較第三方的開(kāi)源框架,原生的XCTest框架性能更好且更穩(wěn)定聘殖,因此這里我們選擇基于swift語(yǔ)言和XCTest框架來(lái)開(kāi)發(fā)晨雳。XCTest框架提供了非常全面的啟動(dòng)App和UI操作相關(guān)的API接口, 因此1奸腺、2兩點(diǎn)完全可以實(shí)現(xiàn)餐禁,當(dāng)然第三點(diǎn)的日志記錄的實(shí)現(xiàn)也同樣不會(huì)有什么問(wèn)題。接下來(lái)就是具體實(shí)施了突照。
首先帮非,我們創(chuàng)建一個(gè)用來(lái)執(zhí)行測(cè)試的主類:StabilityTestRunner,然后再編寫(xiě)代碼去實(shí)現(xiàn)以上三點(diǎn)。
一末盔、持續(xù)隨機(jī)觸發(fā)UI事件
讓我們拆分一下筑舅,隨機(jī)觸發(fā)UI事件,實(shí)際上包含兩部分:隨機(jī)UI元素和隨機(jī)的UI操作陨舱。那么:
隨機(jī)生成UI元素:
func randomElement(of types: [ElementType]) -> XCUIElement? {
var allElement:[XCUIElement] = []
for type in types {
if !self.exists{
break
}
var elements: [XCUIElement]
if self.alerts.count > 0 {
elements = self.alerts.descendants(matching: type).allElementsBoundByIndex
}else {
elements = self.descendants(matching: type).allElementsBoundByIndex
}
let filteredElements = elements.filter { element in
if !element.exists {
return false
}
if !element.isHittable || !element.isEnabled {
return false // Filter out non clickable and blocked elements.
}
return true
}
allElement.append(contentsOf: filteredElements)
}
return allElement.randomElement()
}
隨機(jī)生成UI操作:
/**
Random execution of the given UI operation.
- parameter element: Page Elements.
- parameter actions: Dictionary objects containing different UI operations.
*/
private func performRandomAction(on element: XCUIElement, actions: [String: (XCUIElement) -> ()]) {
let keys = Array(actions.keys)
let randomIndex = Int.random(in: 0..<keys.count)
let randomKey = keys[randomIndex]
let action = actions[randomKey]
if action == nil {
return
}
if !element.exists {
return
}
if !element.isHittable {
return
}
Utils.log("step\(currentStep): \(randomKey) \(element.description)")
action!(element)
}
二翠拣、持續(xù)測(cè)試和崩潰重啟
while !isTestingComplete{
// Randomly select page elements.
let element = app.randomElement(of: elementType)
if element != nil {
currentStep += 1
takeScreenshot(element: element!)
performRandomAction(on: element!, actions: actions) // Perform random UI operations.
XCTWaiter().wait(for: [XCTNSPredicateExpectation(predicate: NSPredicate(format: "self == %d", XCUIApplication.State.runningForeground.rawValue), object: app)], timeout: stepInterval)
if app.state != .runningForeground {
if app.state == .notRunning || app.state == .unknown {
Utils.saveImagesToFiles(images: screenshotData)
Utils.saveImagesToFiles(images: screenshotOfElementData, name: "screenshot_element")
Utils.log("The app crashed. The screenshot before the crash has been saved in the screenshot folder.")
}
app.activate()
}
}
}
三、日志記錄
記錄截圖并標(biāo)記UI元素:
private func takeScreenshot(element: XCUIElement) {
let screenshot = app.windows.firstMatch.screenshot().image
if screenshotData.count == 3 {
let minKey = screenshotData.keys.sorted().first!
screenshotData.removeValue(forKey: minKey)
}
let screenshotWithRect = Utils.drawRectOnImage(image: screenshot, rect: element.frame)
screenshotData[currentStep] = screenshotWithRect.pngData()
let screenshotOfElement = element.screenshot().pngRepresentation
if screenshotOfElementData.count == 3 {
let minKey = screenshotOfElementData.keys.sorted().first!
screenshotOfElementData.removeValue(forKey: minKey)
}
screenshotOfElementData[currentStep] = screenshotOfElement
}
通過(guò)文本日志記錄測(cè)試執(zhí)行過(guò)程:
static func log(_ message: String) {
print(message)
let dateFormatter = DateFormatter()
dateFormatter.dateFormat = "yyyy-MM-dd HH:mm:ss"
let dateString = dateFormatter.string(from: Date())
let fileManager = FileManager.default
do {
try fileManager.createDirectory(atPath: logSavingPath, withIntermediateDirectories: true, attributes: nil)
} catch {
print("Error creating images directory: \(error)")
}
var fileURL: URL
if #available(iOS 16.0, *) {
fileURL = URL.init(filePath: logSavingPath).appendingPathComponent("log.txt")
} else {
fileURL = URL.init(fileURLWithPath: logSavingPath).appendingPathComponent("log.txt")
}
do {
try "\(dateString) \(message)".appendLineToURL(fileURL: fileURL)
} catch {
print("Error writing to log file: \(error)")
}
日志導(dǎo)出:
// To add the log files to the test results file, you can view it on your Mac. The test results file path: /User/Library/Developer/Xcode/DerivedData/AppStability-*/Logs.
let zipFile = "\(NSSearchPathForDirectoriesInDomains(.documentDirectory, .userDomainMask, true)[0])/Logs.zip"
let attachment = XCTAttachment(contentsOfFile: URL(fileURLWithPath: zipFile))
attachment.name = "Logs"
attachment.lifetime = .keepAlways
// Add the "Logs.zip" file to the end of test result file.
add(attachment)
Utils.log("The logs for test steps has been added to the end of test result file at /User/Library/Developer/Xcode/DerivedData/AppStability-*/Logs")
注:以上代碼只是主體實(shí)現(xiàn)游盲,了解相關(guān)細(xì)節(jié)可通過(guò)GitHub或Gitee查閱完整代碼误墓。
總結(jié)
總的來(lái)說(shuō)實(shí)現(xiàn)起來(lái)并不是很困難,當(dāng)然從程序使用角度而言益缎,用戶可自定義隨機(jī)UI事件的UI元素范圍和UI操作的范圍以及測(cè)試執(zhí)行的時(shí)長(zhǎng)和時(shí)間間隔谜慌,因此需要對(duì)ios應(yīng)用程序和Xcode的使用以及iOS UI事件有一定的了解,具體使用可查看完整工程中的示例莺奔。