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() } }
当然以上只是一些简单的思路和测试修改,我们可以根据项目需要进行优化改进。