前面几篇文章介绍了在 Macaca 实践中的一些实用技巧与解决方案,今天简单分析一下 Macaca 的基础原理。这篇文章将以前面所分享的UI 自动化 Macaca-Java 版实践心得中的 demo 为基础,进行一下实例讲解。
通过对源码各个模块的分析,可以帮助我们对 Macaca 的整体构成有一个基础的认识。Macaca 已经开源,相关的源码在对应的 github 上都可以下载:
大家会在 alibaba 集团的开源 github 上找到 macaca 的另一个仓库https://github.com/alibaba/macaca/,关于这两个仓库的关系这里简单讲一下,由于主仓库https://github.com/macacajs模块众多,所以在 alibaba 集团下的 github 上采用了 alibaba/macaca,以方便管理,大家如果需要源码的话需要去 macaca 的主仓库查看,也就是https://github.com/macacajs。
为了方便大家查看,宝宝费了好大劲画了下面这张图 (觉得不错的给个赞吧):
备注:上图所有模块均可以在官方 github 上找到对应的源码 https://github.com/macacajs
Macaca 提供的命令行工具
$macaca server
启动 server
$macaca server --verbose
启动 server 并打印详细日志
$macaca doctor
检验当前 macaca 环境配置
macaca 提供的元素查找工具,可以将 app 视图的结构以布局结构树的格式在浏览器上展示出来,用过点击某个元素,就可以方便的查询到该控件的基本信息,以方便查找。具体使用可参考官网: https://macacajs.com/inspector
macaca 提供的脚本录制工具,可以通过录制获得脚本,对于入门同学很有帮助。https://macacajs.com/recorder
Macaca 是按照经典的 Server-Client 设计模式进行设计的,也就是我们常说的 C/S 架构。WebDriver-server 部分便充当了 server 这部分的角色,他的职责就是等待 client 发送请求并做出响应。
client 端简单来讲就是我们的测试代码,我们测试代码中的一些行为,比如控件查找、点击等,这些行为以 http 请求的方式发送给 server,server 接收请求,并执行相应操作,并在 response 中返回执行状态、返回值等信息。
也正是基于这种经典的 C/S 架构,所以 client 端具有跨语言的特点,macaca-wd,wd.java,wd.py 分别是 Macaca 团队针对 Js Java 以及 Python 的封装,只要能保证 client 端按照指定的要求发送 Http 请求,任意语言都可以。
自动化要在不同的平台上跑,需要有对应平台的驱动,这部分驱动接收到来自 server 的操作命令,驱动各自平台的底层完成对应的操作。
Macaca 针对安卓平台的驱动集合
Macaca 针对 iOS 平台的驱动集合
Macaca 针对 Hybrid 的驱动集合。
Macaca 针对 pc 端网页应用的支持
了解了 Macaca 的组成模块以及他们各自的作用,下面我们看一下各个模块是如何组装起来实现自动化测试流程的,宝宝同样费了很大劲画了一张图如下:
以文章开始提到的 demo 为例 (client 以 Java 版为例) demo 地址
源码克隆到本地并配置好 Macaca 相关环境后,我们来执行一次用例:
➜ bootstrap git:(master) ✗ macaca server --verbose
>> request.js:24:12 [master] pid:5499 get remote update info failed.
>> index.js:17:12 [master] pid:5503 webdriver server start with config:
{ port: 3456,
verbose: true,
always: true,
ip: '30.30.180.23',
host: 'MacBook-Pro.local',
loaded_time: '2016-12-07 17:00:22' }
>> middlewares.js:17:10 [master] pid:5503 base middlewares attached
>> router.js:129:10 [master] pid:5503 router set
>> webdriver sdk launched
从这一步打印的信息我们可以看到,这一步实际上执行的是流程图中第一步的操作,启动 server,建立连接,然后 server 返回所连接的 ip 以及端口号,因为我们是本地跑,所以 ip 实际上是本机的 ip 地址
以 SampleTest 为例,右键执行 junitTest,稍作等待,就会看到系统自动启动了 ios 的模拟器并跑起来了用例。执行过程中的某个截图如下:
首先我们来看一下对应用例启动的 client 端核心代码:
@Before
public void setUp() throws Exception {
// 清除日志记录
ResultGenerator.clearOldData();
//清理截图重新记录
File file = new File(Config.ScreenshotPath);
deleteOldScreen(file);
// 初始化应用基础信息
JSONObject props = new JSONObject();
if (Config.PLATFORM.equals("ios")) {
// 创建ios实例
props.put("app", Config.IOS_APP);
props.put("platformName", Config.IOS_PLATFORM_NAME);
props.put("deviceName", Config.IOS_DEVICE_NAME);
driver.setCurPlatform(PlatformType.IOS);
} else {
//创建安卓实例
props.put("app", Config.ADR_APP);
props.put("platformName", Config.ADR_PLATFORM_NAME);
driver.setCurPlatform(PlatformType.ANDROID);
}
// 覆盖安装
props.put("reuse", Config.REUSE);
JSONObject desiredCapabilities = new JSONObject();
desiredCapabilities.put("desiredCapabilities", props);
driver.initDriver(desiredCapabilities);
}
在这段代码中,我们做的工作是根据不同的平台设置用例的一些基础启动信息,包含平台类型、安装包地址、设备 id、是否覆盖安装等参数,设置完成后,通过 driver.initDriver(desiredCapabilities) 这个操作启动 driver,这个过程便会按照流程图中的第二个步骤发送 http 请求,server 会接收到这个请求并创建一个 session,在这次的用例执行中,所有的操作都会基于这个 session 进行,来看一下针对这个操作控制台所打印的信息 (为方便突出主要过程省略了部分无关日志):
>> responseHandler.js:11:12 [master] pid:5503 Recieve HTTP Request from Client: method: POST url: /wd/hub/session, jsonBody: {"desiredCapabilities":{"app":"/Users/Macaca/github/bootstrap/app/ios-app-bootstrap.zip","reuse":"3","platformName":"iOS","deviceName":"iPhone 6"}}
>> session.js:47:10 [master] pid:5503 Creating session, sessionId: abe8f19c-76ea-4bb0-b5b9-d69e3ce9b798.
>> helper.js:196:12 [master] pid:5503 Unzipping local app form /Users/Macaca/github/bootstrap/app/ios-app-bootstrap.zip
>> macaca-ios.js:194:10 [master] pid:5503 Get available devices(...省略设备列表)
...省略部分信息
>> proxy.js:54:14 [master] pid:5503 Proxy: /session:POST to http://30.30.180.23:8900/session:POST with body: {"desiredCapabilities":{"bundleId":"xudafeng.ios-app-bootstrap","app":"/var/folders/lf/lmrfrj9s4xn76wq_4k3x92380000gn/T/ios-app-bootstrap.app/","platformName":"iOS"}}
>> proxy.js:67:16 [master] pid:5503 Got response with status 200: {"value":{"sessionId":"6A1D2ED3-37BD-449C-A128-2E72DEF4CBF9","capabilities":{"device":"iphone","browserName":"ios-app-bootstrap","sdkVersion":"10.1","CFBundleIdentifier":"xudafeng.ios-app-bootstrap...
>> responseHandler.js:47:14 [master] pid:5503 Send HTTP Respone to Client: {"sessionId":"abe8f19c-76ea-4bb0-b5b9-d69e3ce9b798","status":0,"value":"{\"app\":\"/var/folders/lf/lmrfrj9s4xn76wq_4k3x92380000gn/T/ios-app-bootstrap.app/\",\"reuse\":\"3\",\"platformName\":\"iOS\",\"deviceName\":\"iPhone 6\"}"}
经过如上步骤后,连接便已经成功建立了,下一步再分析一下一个具体操作,以登录为例,对应的 client 端的代码如下:
(因为框架层封装了一些操作,所以代码看上去比较少,具体的控件查找部分看不到,有需要详细了解的可以研究源码)
// SampleTest.java
@Test
public void test () throws Exception {
// 处理登录
LoginPage loginPage = new LoginPage("登录页");
loginPage.setDriver(driver);
if (loginPage.hasPageShown(LoginPageUI.LOGIN_BTN)) {
saveScreen(loginPage.pageDesc);
ResultGenerator.loadPageSucc(loginPage);
loginPage.login("test", "123");
} else {
ResultGenerator.loadPageFail(loginPage);
}
}
对应登录按钮的查询操作,我们会在控制台上看到如下的日志:
>> responseHandler.js:11:12 [master] pid:5503 Recieve HTTP Request from Client: method: POST url: /wd/hub/session/abe8f19c-76ea-4bb0-b5b9-d69e3ce9b798/element, jsonBody: {"using":"name","value":"Login"}
>> proxy.js:54:14 [master] pid:5503 Proxy: /wd/hub/session/abe8f19c-76ea-4bb0-b5b9-d69e3ce9b798/element:POST to http://30.30.180.23:8900/session/6A1D2ED3-37BD-449C-A128-2E72DEF4CBF9/element:POST with body: {"using":"name","value":"Login"}
>> proxy.js:67:16 [master] pid:5503 Got response with status 200: {"value":{"using":"name","value":"Login","description":"unable to find an element"},"sessionId":"abe8f19c-76ea-4bb0-b5b9-d69e3ce9b798","status":7}
>> session.js:107:14 [master] pid:5503 Send HTTP Respone to Client: {"value":"{\"using\":\"name\",\"value\":\"Login\",\"description\":\"unable to find an element\"}","sessionId":"abe8f19c-76ea-4bb0-b5b9-d69e3ce9b798","status":7}
在上面的日志中我们可以看到,当我们查找登录按钮的时候,client 发送了一个 http 请求给 server,请求的操作是 element(这个表示控件查找),参数是{"using":"name","value":"Login"},这是告诉 server 我们要找的这个控件的 name 属性是 Login,server 收到这个请求,通过 router 路由转发给 iOS 的驱动 (在启动 driver 的时候已经设置的平台类型,因此这里能知道找 ios),iOS 驱动收到请求驱动 XCUITest 框架对模拟器上的目标 app 执行对应的控件查找操作,得到 response 后原路返回给 client,这样就完成了一次请求的完整的生命周期。
关于 Macaca 使用中的问题,很多同学会直接去社区里提问,但是很多时候大家问的问题都是类似的,这种情况建议大家先去查看一下官方仓库的 issues,看看有没有人遇到自己同样问题的,如果没有,可以新建 issue。查看 issue 之前先看下自己问题所属的模块,比如如果问题在 wd.java 的使用中,可以去 wd.java 仓库下查看 issue:https://github.com/macacajs/wd.java/issues
如上简单总结了 Macaca 的基础原理,提供一个小窍门是大家可以对照控制台输出与文章中的流程图一一对应,这样就能大体了解整个流程的数据流向,从而就能参透 Macaca 的基础原理了。个人水平有限,如有不当,欢迎指正。
后面会陆续放出自己在实践中的其他心得与经验,敬请期待,也欢迎大家交流讨论。
“Macaca 开源社区” 群的钉钉群号: 11775486(欢迎入群讨论,钉钉顶部搜索框搜索群号加入即可)
UI 自动化框架调研总结
从无到有搭建 Macaca 环境 (forMac)
Macaca-Java 版入门指南
UI 自动化 Macaca-Java 版实践心得
UI 自动化利器 - 为你的应用自动添加控件 ID 探索
Macaca 基础原理浅析