06 Sonic - 实现多设备运行
我们做的是音视频的app,和一般app不同需要一个对手。例如,一台设备推流后,需要另一台设备拉流,这样一般自动化平台和sonic都不能满足需求。所以才想基于sonic的基础上,稍加修改来实现这个需求。
一、现状分析
sonic现有功能。
1、在测试用例管理中,创建两个用例。case1 打开app并点击一个按钮。case2点击返回按钮并点击一个按钮。
2、创建两个套件,test 套件调用case,test2 套件调用case2。
3、先运行 test 再运行 test2。这样就初步实现一个case用不同设备运行的效果。但有两个问题
- 每次运行套件都需要 Appium server ,每次启动都需要10S左右,有点慢
- 不知道 test 套件1啥时候运行完成,也就是不知道啥时候运行 test2 套件
- 同一个case的运行结果显示到了两个结果里
- 同一个case拆的有点乱,脚本看着有些费劲。脚本要是多了,看着更乱。
二、处理路线图
1、只改 web + server 端。修改 runSuite 接口调用完case1再屌用case2,实现一次调用运行两个case。需要创建一个线程判断case1运行完成后再调用case2
2、配合修改 Agent ,创建 Appium server 后保持,直到收到断开命令后再释放 Appium server。修改上报运行结果,将多个case的结果报到一起
以上两步实现后,基本就可以满足需求了。
3、重写sonic用例编辑,实现添加步骤的同时指定设备类型。
4、重写测试套件运行,运行时重新整理case实现多设备,多设备类型运行case。
三、只改 web + server 端。修改 runSuite 接口调用完case1再调用case2
TestSuitesServiceImpl 的 218 行 NettyServer.getMap().get(id).writeAndFlush(result.toJSONString()); 将构造的数据发给的agent,所以可以在这里处理
3.1、同步 发送 test 套件,休眠20S后再发送 test2 套件
1、web上运行 test 套件
2、调试模式在 TestSuitesServiceImpl.java 的 172 行打断点,这里就是在数据库中生成一个测试结果记录。
看到改动的表是 results
3、在 180 行打断点,记录的是发个 agent 的运行脚本,如下:
test套件发的内容如下:
{"msg":"suite","cases":[{"gp":{},"rid":14,"steps":[{"step":{"caseId":1,"conditionType":0,"content":"","elements":[],"error":3,"id":1,"parentId":0,"platform":1,"projectId":1,"sort":1,"stepType":"openApp","text":"com.example.tablayouttest"}},{"step":{"caseId":1,"conditionType":0,"content":"","elements":[{"eleName":"点击 quickstart","eleType":"xpath","eleValue":"//android.widget.Button[@text='QUICKSTART']","id":1,"projectId":1}],"error":3,"id":2,"parentId":0,"platform":1,"projectId":1,"sort":2,"stepType":"click","text":""}}],"device":[{"agentId":1,"chiName":"","cpu":"arm64-v8a","gear":0,"id":1,"imgUrl":"","manufacturer":"Meizu","model":"M1822","name":"meizu_M1822_CN","nickName":"","password":"","platform":1,"position":0,"size":"1080x2160","status":"ONLINE","udId":"822QEDU5227H3","user":"lrs","version":"8.1.0"}],"cid":1}],"pf":1}
test2套件发的内容如下:
{"msg":"suite","cases":[{"gp":{},"rid":20,"steps":[{"step":{"caseId":2,"conditionType":0,"content":"BACK","elements":[],"error":3,"id":3,"parentId":0,"platform":1,"projectId":1,"sort":3,"stepType":"keyCode","text":""}},{"step":{"caseId":2,"conditionType":0,"content":"","elements":[{"eleName":"点击 quickstart","eleType":"xpath","eleValue":"//android.widget.Button[@text='QUICKSTART']","id":1,"projectId":1}],"error":3,"id":4,"parentId":0,"platform":1,"projectId":1,"sort":4,"stepType":"click","text":""}}],"device":[{"agentId":1,"chiName":"","cpu":"arm64-v8a","gear":0,"id":1,"imgUrl":"","manufacturer":"Meizu","model":"M1822","name":"meizu_M1822_CN","nickName":"","password":"","platform":1,"position":0,"size":"1080x2160","status":"ONLINE","udId":"822QEDU5227H3","user":"lrs","version":"8.1.0"}],"cid":2}],"pf":1}
4、动手开始修改代码,在 TestSuitesServiceImpl.java 的 181 行代码修改如下
// 发送 test 套件 String testJsonStr = "{\"msg\":\"suite\",\"cases\":[{\"gp\":{},\"rid\":14,\"steps\":[{\"step\":{\"caseId\":1,\"conditionType\":0,\"content\":\"\",\"elements\":[],\"error\":3,\"id\":1,\"parentId\":0,\"platform\":1,\"projectId\":1,\"sort\":1,\"stepType\":\"openApp\",\"text\":\"com.example.tablayouttest\"}},{\"step\":{\"caseId\":1,\"conditionType\":0,\"content\":\"\",\"elements\":[{\"eleName\":\"点击 quickstart\",\"eleType\":\"xpath\",\"eleValue\":\"//android.widget.Button[@text='QUICKSTART']\",\"id\":1,\"projectId\":1}],\"error\":3,\"id\":2,\"parentId\":0,\"platform\":1,\"projectId\":1,\"sort\":2,\"stepType\":\"click\",\"text\":\"\"}}],\"device\":[{\"agentId\":1,\"chiName\":\"\",\"cpu\":\"arm64-v8a\",\"gear\":0,\"id\":1,\"imgUrl\":\"\",\"manufacturer\":\"Meizu\",\"model\":\"M1822\",\"name\":\"meizu_M1822_CN\",\"nickName\":\"\",\"password\":\"\",\"platform\":1,\"position\":0,\"size\":\"1080x2160\",\"status\":\"ONLINE\",\"udId\":\"822QEDU5227H3\",\"user\":\"lrs\",\"version\":\"8.1.0\"}],\"cid\":1}],\"pf\":1}"; NettyServer.getMap().get(id).writeAndFlush(testJsonStr); try { sleep(20000); // 发送 test2 套件 String test2JsonStr = "{\"msg\":\"suite\",\"cases\":[{\"gp\":{},\"rid\":20,\"steps\":[{\"step\":{\"caseId\":2,\"conditionType\":0,\"content\":\"BACK\",\"elements\":[],\"error\":3,\"id\":3,\"parentId\":0,\"platform\":1,\"projectId\":1,\"sort\":3,\"stepType\":\"keyCode\",\"text\":\"\"}},{\"step\":{\"caseId\":2,\"conditionType\":0,\"content\":\"\",\"elements\":[{\"eleName\":\"点击 quickstart\",\"eleType\":\"xpath\",\"eleValue\":\"//android.widget.Button[@text='QUICKSTART']\",\"id\":1,\"projectId\":1}],\"error\":3,\"id\":4,\"parentId\":0,\"platform\":1,\"projectId\":1,\"sort\":4,\"stepType\":\"click\",\"text\":\"\"}}],\"device\":[{\"agentId\":1,\"chiName\":\"\",\"cpu\":\"arm64-v8a\",\"gear\":0,\"id\":1,\"imgUrl\":\"\",\"manufacturer\":\"Meizu\",\"model\":\"M1822\",\"name\":\"meizu_M1822_CN\",\"nickName\":\"\",\"password\":\"\",\"platform\":1,\"position\":0,\"size\":\"1080x2160\",\"status\":\"ONLINE\",\"udId\":\"822QEDU5227H3\",\"user\":\"lrs\",\"version\":\"8.1.0\"}],\"cid\":2}],\"pf\":1}"; NettyServer.getMap().get(id).writeAndFlush(test2JsonStr); } catch (InterruptedException e) { e.printStackTrace(); } //NettyServer.getMap().get(id).writeAndFlush(result.toJSONString());
5、数据库执行如下sql
select * from results where id=14; // 测试结果 UPDATE results set end_time=null, receive_msg_count=0, `status`=0 where id=14; // 恢复测试结果数据 DELETE FROM result_detail WHERE result_id=14; // 删除上一次运行结果 select * from results where id=20 // 测试结果 UPDATE results set end_time=null, receive_msg_count=0, `status`=0 where id=20; // 恢复测试结果数据 DELETE FROM result_detail WHERE result_id=20; // 删除上一次运行结果
6、web页面运行套件 test
点击后在agent中就能看到运行test套件后又继续运行test2套件
3.1、异步 发送 test 套件,判断 test 套件完成后,再发送 test2 套件
cachedThreadPool.execute(() -> { while (true) { try { sleep(1000); Results results1 = resultsService.getById(14); if (results1.getStatus() > 0) { System.out.println("==== 运行套件 test2"); // 发送 test2 套件 String test2JsonStr = "{\"msg\":\"suite\",\"cases\":[{\"gp\":{},\"rid\":20,\"steps\":[{\"step\":{\"caseId\":2,\"conditionType\":0,\"content\":\"BACK\",\"elements\":[],\"error\":3,\"id\":3,\"parentId\":0,\"platform\":1,\"projectId\":1,\"sort\":3,\"stepType\":\"keyCode\",\"text\":\"\"}},{\"step\":{\"caseId\":2,\"conditionType\":0,\"content\":\"\",\"elements\":[{\"eleName\":\"点击 quickstart\",\"eleType\":\"xpath\",\"eleValue\":\"//android.widget.Button[@text='QUICKSTART']\",\"id\":1,\"projectId\":1}],\"error\":3,\"id\":4,\"parentId\":0,\"platform\":1,\"projectId\":1,\"sort\":4,\"stepType\":\"click\",\"text\":\"\"}}],\"device\":[{\"agentId\":1,\"chiName\":\"\",\"cpu\":\"arm64-v8a\",\"gear\":0,\"id\":1,\"imgUrl\":\"\",\"manufacturer\":\"Meizu\",\"model\":\"M1822\",\"name\":\"meizu_M1822_CN\",\"nickName\":\"\",\"password\":\"\",\"platform\":1,\"position\":0,\"size\":\"1080x2160\",\"status\":\"ONLINE\",\"udId\":\"822QEDU5227H3\",\"user\":\"lrs\",\"version\":\"8.1.0\"}],\"cid\":2}],\"pf\":1}"; NettyServer.getMap().get(id).writeAndFlush(test2JsonStr); break; } else { System.out.println("sleep 1S ================="); } } catch (InterruptedException e) { e.printStackTrace(); } } });
2、在 TestSuitesServiceImpl.java 的 开头增加
private ExecutorService cachedThreadPool = Executors.newCachedThreadPool();
3、继续在数据库执行sql,清理上一次运行记录
4、web页面运行 test 套件,从日志中就能看到每隔 1S 查一下数据库直到 test 套件运行完成后,就运行 test2 套件。
四、配合修改 agent
4.1、保持 Appium server
1、复制 org.cloud.sonic.agent.common.maps 目录下的 HandlerMap.java 为 HandlerRunSuiteMap.java 备用。这个 map 用来保存 androidStepHandler
2、将 AndroidTestTaskBootThread.java 文件的201行 androidStepHandler.startAndroidDriver(udId); 改为
if (HandlerRunSuiteMap.getAndroidMap().get(udId) == null) { // 按照设备 uuid 查找之前是否初始化过 androidStepHandler.startAndroidDriver(udId); // 没有初始化 HandlerRunSuiteMap.getAndroidMap().put(udId, androidStepHandler); // 初始化后 put 到 map 中 } else { androidStepHandler = HandlerRunSuiteMap.getAndroidMap().get(udId); // 之前初始化过,从 map 中取出使用 }
3、246 行将 androidStepHandler.closeAndroidDriver(); 注释。这句的意思是在一个 suite 结束后关闭 appium
4、再次运行 test 套件,在日志中看到 先后执行test 套件和 test2 套件,但只启动了一次 appium
5、到此同一个设备无论 runSuite 多少次,也只会启动一个 appium 实例,这样就解决多次调用需要等很久的问题。不过这里有一个bug,我将 closeAndroidDriver 注释了,这样一直没有释放 appium,后续还要加一个 step ,启动 appium、停止 appium
4.2、修改上报运行结果,将多个case的结果报到一起
1、在 LogUtil.java 的 55 行添加如下代码
if (resultId == 20) { // 如果是 test2 套件 resultId = 14; // 临时将 test2 套件的id 20 改为 test套件的id 14 }
2、再次运行 test 套件,在日志中看到无论 test 套件还是 test2套件上报的 rid 都是14
3、在数据库中看到, 只有 result_id 为 14 的上报
4.3、修改下发 suite 数据,将多个case的结果报到一起
2、还记得上边通过修改server 中的 TestSuitesServiceImpl.java 文件,实现 test 套件运行完后运行 test2 套件的吗?就是改哪里。将 197 行的 rid:20 改为 14
3、重起 controller 后,再次运行 test 套件,同样在日志里看到两个套件上报的都是 rid=14,数据库中看到了也是 7 条 rid=14 的记录
所以后边只要修改下发数据,即可实现多个设备运行同一个 case ~~~
4.4、增加 step ,关闭 appium
1、在 agent 的 AndroidStepHandler.java 的 1485 增加如下代码
case "closeDriver" : // 增加 closeDriver 的 step closeAndroidDriver(); // 调用关闭 appium 的方法 break;
2、在 AndroidStepHandler.java 的 192 行的 closeAndroidDriver 方法中增加 清除 HandlerRunSuiteMap 代码
HandlerRunSuiteMap.getAndroidMap().remove(log.udId); // appium 关闭成功后清除对应 udid HandlerRunSuiteMap
3、在server 中的 TestSuitesServiceImpl.java 文件的 197 行中增加关闭 appium 的测试步骤,如下
{"step": {"caseId": 10,"conditionType": 0, "content": "", "elements": [], "error": 3,"id": 5,"parentId": 0,"platform": 1,"projectId": 1,"sort": 5,"stepType": "closeDriver","text": ""}}
完整 test2JsonStr 如下
String test2JsonStr = "{\"msg\":\"suite\",\"cases\":[{\"gp\":{},\"rid\":14,\"steps\":[{\"step\":{\"caseId\":2,\"conditionType\":0,\"content\":\"BACK\",\"elements\":[],\"error\":3,\"id\":3,\"parentId\":0,\"platform\":1,\"projectId\":1,\"sort\":3,\"stepType\":\"keyCode\",\"text\":\"\"}},{\"step\":{\"caseId\":2,\"conditionType\":0,\"content\":\"\",\"elements\":[{\"eleName\":\"点击 quickstart\",\"eleType\":\"xpath\",\"eleValue\":\"//android.widget.Button[@text='QUICKSTART']\",\"id\":1,\"projectId\":1}],\"error\":3,\"id\":4,\"parentId\":0,\"platform\":1,\"projectId\":1,\"sort\":4,\"stepType\":\"click\",\"text\":\"\"}},{\"step\": {\"caseId\": 10,\"conditionType\": 0, \"content\": \"\", \"elements\": [], \"error\": 3,\"id\": 5,\"parentId\": 0,\"platform\": 1,\"projectId\": 1,\"sort\": 5,\"stepType\": \"closeDriver\",\"text\": \"\"}}],\"device\":[{\"agentId\":1,\"chiName\":\"\",\"cpu\":\"arm64-v8a\",\"gear\":0,\"id\":1,\"imgUrl\":\"\",\"manufacturer\":\"Meizu\",\"model\":\"M1822\",\"name\":\"meizu_M1822_CN\",\"nickName\":\"\",\"password\":\"\",\"platform\":1,\"position\":0,\"size\":\"1080x2160\",\"status\":\"ONLINE\",\"udId\":\"822QEDU5227H3\",\"user\":\"lrs\",\"version\":\"8.1.0\"}],\"cid\":2}],\"pf\":1}";
4、重起后重新运行 test 套件,这时候在日志里就看到运行 test 套件的时候启动了 appium,在 test2 套件运行完成后运行了关闭了 appium
到此,基本实现了多个设备完哼一个测试用例的需求了。但只是演示代码,还不能用于生产,比如:
1、创建appium的方法不太好,可以参考远控的测试用例执行的方案做
2、关闭 appium 的方法就不太好
3、还有很多异常处理的地方没有关闭 appium 等等
4、在上边的 “处理路线图” 中讲的,web页面还有很多体力活工作。