swiftmonkey 源碼剖析及二次開發思路


swift monkey是用來在iOS端進行monkey測試的,用swift語言編寫,基於XCTest測試框架,調用私有api XCEventGenerator,不斷生成event事件,不過在Xcode10.1以上XCTestFramework已經去掉了這個API,所以如果是想在10.1以上使用的話需要進行二次開發。
 
在使用Android端的monkey的時候就發現不同的app對monkey測試的需求是不同的,基本都需要對原生的工具框架進行二次開發來滿足不同的測試需求,Android的話fastmonkey基本可以滿足一些定制化場景了,但是iOS這邊還不夠,因此我們查看下swiftmonkey源碼,根據自己需要進行二次開發。
 
具體的使用步驟就不多贅述了,網上的資源也很多,就記錄一個github地址吧
 
其實發現這個工具也有段時間沒有更新了。
 

框架構成



簡單介紹下整個工具的文件構成。
 
Monkey: 是程序入口,主要是monkey構造,monkey運行等
 
MonkeyXCTest: 看注釋的話本來是要擴展monkey使用公共的XCTest API來生成事件的,但是沒寫。。。
 
MonkeyXCTestPrivate:這塊才是利用私有API生成各種事件的代碼
 
MonkeyUIAutomation: 這塊是利用UIautomation框架來執行各種事件的,但是只支持模擬器
 
Random: 這塊是生成各種隨機數的函數
 
 

主要部分源碼分析


 
monkeyAround執行方法(按次數執行,按時間執行),通過循環生成隨機事件
 
 publicfuncmonkeyAround(iterations: Int) {
        for_in1... iterations {
            actRandomly()
            actRegularly()
        }
    }

 
actRandomly( )  是將添加的隨機事件執行
actRegular( ) 是固定間隔執行事件
 
 
/// Generate one random event.
    publicfuncactRandomly() {
        letx = r.randomDouble() * totalWeight
        foraction inrandomActions{
            ifx < action.accumulatedWeight {
                action.action()
                return
            }
        }
    }
 
    /// Generate any pending fixed-interval events.
    publicfuncactRegularly() {
        actionCounter+= 1
 
        foraction inregularActions{
            ifactionCounter% action.interval == 0{
                action.action()
            }
        }
    }

 

 
可以看到是從random隨機事件數組中取出action然后執行,在添加事件的時候需要添加事件所占比例,在事件執行的時候也會根據事件比例去執行。
 
那么事件是從哪里隨機生成的呢?
 
在使用monkey的時候,需要添加隨機事件。
例如:
monkey.addDefaultXCTestPrivateActions()
 
添加默認XCTest私有事件,查看詳細方法,可以看到添加的隨機事件的比例
 
publicfuncaddDefaultXCTestPrivateActions() {
        addXCTestTapAction(weight: 25)
        addXCTestLongPressAction(weight: 1)
        addXCTestDragAction(weight: 0)
        addXCTestPinchCloseAction(weight: 0)
        addXCTestPinchOpenAction(weight: 0)
        addXCTestRotateAction(weight: 0)
        //addXCTestOrientationAction(weight: 1) // TODO: Investigate why this does not work.
}
 
可以看到它默認給我們添加了幾個event,並且設置了權重
那來看下第一個event,tapAction是怎么添加的
 
在addXCTestTapAction方法里,添加了一個閉包函數,生成了隨機的point,然后調用XCEventGenerator來執行,函數比較長就不粘貼了。
 
值得注意的一點是,addXCTestTapAction中是調用了addAction方法來添加事件到隨機數組,然后在執行時遍歷執行
 
在addAction方法中還有一個點是里面又嵌套了個閉包函數用來監聽當前application始終是我們要測試的app,如果發現因為調用一些系統手勢或事件導致退出app,會重新拉回。
 
funcactInForeground(_action: @escapingActionClosure) -> ActionClosure{
        return{
            guard#available(iOS9.0, *) else{
                action()
                return
            }
            letclosure: ActionClosure= {
                // state來判斷當前app執行狀態
                ifXCUIApplication().state!= .runningForeground{
                    XCUIApplication().activate()
                }
                action()
            }
            ifThread.isMainThread{
                closure()
            } else{
                DispatchQueue.main.async(execute: closure)
            }
        }
    }

 

 
至此我們可以理出swiftmonkey的執行過程
1.初始化monkey
2.添加隨機事件,設置權重
3.執行monkey
 

二次開發思路

 
如何二次開發?
 

以解決swiftmonkey插樁到app代碼的問題為例。

 
常規的使用方法是將monkey添加到我們自己的項目中才能執行,但是當我們理解了它的原理就可以稍微改造下。
 
swiftmonkey是基於xcuitest來執行的,因此首先需要在項目中由xcuiapplication吊起測試的app,然后隨機執行。但是如果了解XCTest 的話就知道XCTest是支持吊起其它app的,只要傳入app的bundleIdentifier就可以,因此我們可以隨便建一個Xcode項目,然后導入swiftmonkey文件,創建uitest文件,但是啟動monkey前指定我們需要測試app的bundleid就可以了。
 
例如:
letapp2 = XCUIApplication(bundleIdentifier: "com.myapp.app")
app2.launch()
 
但是執行后發現還是會拉回到創建的這個假app,為什么呢,分析源碼的時候說過一個點,在每次執行事件的時候都會判斷一下當前app(monkey所在項目)是否啟動活躍在前台,如果不是就會拉起,那就好了我們把判斷的application改成自己實際要測試的app就可以了
 
例如:
在actInForeground方法中,把application改成實際要測試的
 
原來是:
if XCUIApplication().state!= .runningForeground{
       XCUIApplication().activate()
}

 

 
改成:
 
if XCUIApplication(bundleIdentifier: "com.myapp.app").state!= .runningForeground{
     XCUIApplication(bundleIdentifier: "com.myapp.app").activate()
}

 

 
這樣每次就會判斷實際要測試的app是否在前台運行,如果不是會自動拉起。
 
經過上面的改造swiftmonkey就不需要插樁了。
 

替換私有API,使之支持Xcode10.1以上

 
我做了個測試,如果使用公有API確實速度慢了很多。
 
以執行50次tapAction為例
使用公共API的速度:14秒左右,大約每秒3-4個action
使用私有API的速度:5秒左右,大約每秒10個action
 
速度相比還是差距比較大的,但是個人覺得如果是app測試而不是手機測試,沒必要過分追求多大的壓力測試,每秒3-4個action的已經超出用戶常規的app操作頻率了
 
通過修改addXCTestTapAction​方法
原來是:
let semaphore = DispatchSemaphore(value: 0)
//            self!.sharedXCEventGenerator.tapAtTouchLocations(locations, numberOfTaps: numberOfTaps, orientation: orientationValue) {
//                semaphore.signal()
//            }
//            semaphore.wait()

 

 
改成:
 
if #available(iOS9.0, *) {
                letapp = XCUIApplication()
                letcoordinate = app.coordinate(withNormalizedOffset: CGVector(dx: locations[0].x/(app.frame.maxX/2), dy: locations[0].y/(app.frame.maxY/2)))
                coordinate.tap()
 
            } else{
                // Fallback on earlier versions
            }
 

 

 如何插入業務邏輯代碼

例如判斷登錄,然后如果沒有登錄就先執行登錄操作

有幾種解決方案:

1.  每次執行event前判斷是否增加業務功能代碼,直接執行功能代碼

2. 插入定時循環事件來執行功能代碼,swiftmonkey有兩種執行事件的方式,

actRandomly( )  是將添加的隨機事件執行
actRegular( ) 是固定間隔執行事件,可以在這里面增加事件,特定的功能邏輯事件
 
以第一種為例:
func actInForeground(_ action: @escaping ActionClosure) -> ActionClosure {
        return {
            guard #available(iOS 9.0, *) else {
                action()
                return
            }
            let closure: ActionClosure = {
//                if XCUIApplication(bundleIdentifier: "com.sanjieke.app").state != .runningForeground {
//                    XCUIApplication(bundleIdentifier: "com.sanjieke.app").activate()
//                }
                if self.currentApp.state != .runningForeground {
                    self.currentApp.activate()
                }
                //判斷是否需要登錄
                if self.currentApp.buttons["密碼登錄"].exists{
                    self.login(app: self.currentApp, user: "15122223333", password: "654321")
                }
                action()
            }
            if Thread.isMainThread {
                closure()
            } else {
                DispatchQueue.main.async(execute: closure)
            }
        }
    }
func login(app: XCUIApplication, user: String, password: String) {
        
        if app.buttons["密碼登錄"].exists {
            
            let button = app.buttons["密碼登錄"]
            button.tap()
            
            let textField = app.textFields["輸入手機號"]
            textField.tap()
            textField.typeText(user)
            
            let secureTextField = app.secureTextFields["輸入密碼"]
            secureTextField.tap()
            secureTextField.typeText(password)
            
            let login = app.buttons["登錄按鈕"]
            login.tap()
            
        }
    }

  

 

當然以上只是一些簡單的思路和測試修改,我們可以根據項目需要進行優化改進。

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM