app穩定性測試-iOS篇

汪汪隊大隊長發表於2023-04-06

穩定性測試:測試應用程式在長時間執行過程中是否存在記憶體洩漏、崩潰等問題,以確保應用程式具有較高的穩定性和可靠性。

對於安卓端,官方提供了很好的穩定性測試工具:monkey。 相比較而言,iOS則沒有,而且當前網路上似乎也沒有很好的第三方工具可以使用,因此只能自己寫了。

我們要開發的iOS穩定性測試程式,應該至少包含以下內容:

  1. 持續隨機觸發UI事件
  2. 崩潰重啟,測試不中斷
  3. 日誌記錄

首先我們確定以上設想的可行性,然後再製定實施方案。在iOS原生開發語言swift和object-C中提供了可進行單元測試和UI測試的XCTest框架,而同樣可進行移動端UI測試的第三方框架還有Appium等,但相比較第三方的開源框架,原生的XCTest框架效能更好且更穩定,因此這裡我們選擇基於swift語言和XCTest框架來開發。XCTest框架提供了非常全面的啟動App和UI操作相關的API介面, 因此1、2兩點完全可以實現,當然第三點的日誌記錄的實現也同樣不會有什麼問題。接下來就是具體實施了。

首先,我們建立一個用來執行測試的主類:StabilityTestRunner,然後再編寫程式碼去實現以上三點。

一、持續隨機觸發UI事件

讓我們拆分一下,隨機觸發UI事件,實際上包含兩部分:隨機UI元素和隨機的UI操作。那麼:

隨機生成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()
    }

隨機生成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)
    }

二、持續測試和崩潰重啟

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

三、日誌記錄

記錄截圖並標記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
    }

透過文字日誌記錄測試執行過程:

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

日誌匯出:

// 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")

注:以上程式碼只是主體實現,瞭解相關細節可透過GitHubGitee查閱完整程式碼。

總結

總的來說實現起來並不是很困難,當然從程式使用角度而言,使用者可自定義隨機UI事件的UI元素範圍和UI操作的範圍以及測試執行的時長和時間間隔,因此需要對ios應用程式和Xcode的使用以及iOS UI事件有一定的瞭解,具體使用可檢視完整工程中的示例。

相關文章