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

  

 

当然以上只是一些简单的思路和测试修改,我们可以根据项目需要进行优化改进。

 

posted @ 2019-08-01 00:27  不当咸鱼  阅读(1219)  评论(1编辑  收藏