安卓端-APPUI自动化实战【下】
上一篇介绍了在solopi端的二次开发内容,接下来介绍下服务端的实现原理。
框架介绍:
使用比较成熟封装度较高的开源框架,尽量减少二次开发难度:Pear Admin Boot: 🍃 基 于 Spring Boot 生 态 , 权 限 , 工 作 流 的 开 发 平 台 (gitee.com)
该框架以 layUI+springboot为脚手架进行开发。
服务端主要实现功能:
1.与客户端(solopi)进行websocket通信,可正常发出&接收消息,
2.管理客户端上传的设备信息,判断当前设备是否在线,方便后续用例下发执行,
3.管理客户端上传的用例信息,用例中心化管理,解决用例不同设备需要多次录制的问题,
4.可选择用例并进行模板替换后,顺序下发到指定设备的solopi端执行,
5.接收solopi端上传的测试报告并展示在前端页面,方便查看和回溯。
websocket通信&接收客户端消息相关:
首先配置websocket请求地址:如下配置时,客户端请求地址则为:ws://xxx.xxx.xxx.xxx:8080/reletime
@Component @EnableWebSocket public class WebSocketConfig implements WebSocketConfigurer { @Autowired RealTimeHandler realTimeHandler; @Override public void registerWebSocketHandlers(WebSocketHandlerRegistry webSocketHandlerRegistry) { webSocketHandlerRegistry.addHandler(realTimeHandler, "realtime") .setAllowedOrigins("*"); } }
1.接收solopi上传的设备信息并进行存储,以序列号作为唯一标识,并将设备状态设置为true(在线):
//接收到的消息转成map Map<String, Object> messageMap = (Map<String, Object>) JSONUtil.parse(payload); for (String s : messageMap.keySet()) { String jsonString = messageMap.get(s).toString(); if ("deviceInfo".equals(s)) { DeviceInfo deviceInfo = JSONObject.toJavaObject(JSONObject.parseObject(jsonString), DeviceInfo.class); String serial = deviceInfo.getSerial();//获取到solopi上传的设备序列号 online(serial, session); DeviceInfo selectDeviceByMac = deviceInfoService.selectDeviceBySerial(serial); if (selectDeviceByMac == null) { deviceInfo.setStatus(true); deviceInfo.setSessionId(session.getId()); deviceInfo.setUpdate_Time(new Date()); deviceInfo.setCreateTime(new Date()); deviceInfoService.insertDeviceInfo(deviceInfo); } else { deviceInfo.setId(selectDeviceByMac.getId()); deviceInfo.setStatus(true); deviceInfo.setSessionId(session.getId()); deviceInfo.setUpdate_Time(new Date()); deviceInfoService.updateDeviceInfo(deviceInfo); } }
断开连接时,更新设备状态为false(离线)状态:
@Override public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception { log.info("断开链接"); DeviceInfo deviceInfo = deviceInfoService.selectDeviceBySessionId(session.getId()); deviceInfo.setStatus(false); deviceInfo.setUpdate_Time(new Date()); deviceInfoService.updateDeviceInfo(deviceInfo); offline(deviceInfo.getSerial(), session); super.afterConnectionClosed(session, status); }
并封装了online和offline方法来处理存储的session:
public static HashMap<String, WebSocketSession> SESSION_POOL = new HashMap(); public static HashMap<String, String> SERIAL_SESSIONID_MAPPING = new HashMap<>(); public static HashMap<String, String> SESSIONID_SERIAL_MAPPING = new HashMap<>(); private void online(String serial, WebSocketSession session) { SESSION_POOL.put(serial, session);//将序列号 token 存到map,针对设备下发命令时使用 SERIAL_SESSIONID_MAPPING.put(serial, session.getId()); SESSIONID_SERIAL_MAPPING.put(session.getId(), serial); } private void offline(String serial, WebSocketSession session) { SESSION_POOL.remove(serial); SERIAL_SESSIONID_MAPPING.remove(serial); SESSIONID_SERIAL_MAPPING.remove(session.getId()); }
2.接收客户端上传的用例信息,以用例名作为唯一标识,对用例进行新建or更新
else if ("caseInfo".equals(s)) { JSONArray jsonArray = JSONUtil.parseArray(jsonString); //遍历每一个case,根据caseName和targetAppPackage判断是否已经存在,不存在则插入,存在则更新 for (int i = 0; i < jsonArray.size(); i++) { cn.hutool.json.JSONObject jsonObject = jsonArray.getJSONObject(i); String caseStr = JSONUtil.toJsonStr(jsonObject); CaseInfo caseInfo = JSONUtil.toBean(jsonObject, CaseInfo.class); CaseInfo caseInfoObj = caseInfoService.selectCaseInfoByNameAndTargetApp(caseInfo.getCaseName(), caseInfo.getTargetAppPackage()); if (caseInfoObj == null) {//不存在 caseInfo.setCreateTime(new Date()); caseInfo.setUpdateTime(new Date()); caseInfo.setCaseinfo(caseStr); caseInfoService.insertCaseInfo(caseInfo);//插入 } else {//存在 caseInfo.setId(caseInfoObj.getId()); caseInfo.setUpdateTime(new Date()); caseInfo.setCaseinfo(caseStr); caseInfoService.updateCaseInfo(caseInfo);//更新 } } }
服务端下发测试用例:
选择用例、选择设备点击执行,生成一条任务。
任务:设备=1:N
任务:用例=1:N
任务:报告=1:N
数据库设计:
核心代码:
//TODO:模板替换规则不对,不应该写死登录注册的模板key,需要根据用户选择的模板进行替换 @PostMapping("execute") @ResponseBody public int execute(@RequestBody CaseExecute caseExecute) { //1.创建任务 SysUser sysUser = (SysUser) SecurityUtil.currentUserObj();//获取当前用户 Task task = new Task(); task.setId(SequenceUtil.makeStringId()); task.setCasecount(Long.valueOf(caseExecute.getCaseIds().size())); task.setDeviceinfocount(Long.valueOf(caseExecute.getDeviceIds().size())); task.setCreatetime(new Date()); task.setUpdatetime(new Date()); task.setTaskstatus(0); task.setCreateId(sysUser.getUserId()); task.setTaskname(caseExecute.getTaskname()); //2.任务关联的caseInfo信息 TaskCaseInfo taskCaseInfo = new TaskCaseInfo(); ArrayList<String> caseIds = caseExecute.getCaseIds(); caseIds.forEach((caseId) -> { taskCaseInfo.setId(SequenceUtil.makeStringId()); taskCaseInfo.setCaseinfoid(caseId); taskCaseInfo.setTaskid(task.getId()); taskCaseInfo.setCreatetime(new Date()); taskCaseInfo.setUpdatetime(new Date()); //入库 taskCaseInfoService.insertTaskCaseInfo(taskCaseInfo); }); //已使用的模板ID、模板值 List<String> alreadyValues = new ArrayList<>(); List<String> alreadyIds = new ArrayList<>(); //查出所有可用的模板信息 Template template = new Template(); template.setKey("login.phone,login.pwd"); List<Template> templates = templateService.selectTemplateList(template); //3.任务关联的deviceInfo TaskDeviceInfo taskDeviceInfo = new TaskDeviceInfo(); ArrayList<String> deviceIds = caseExecute.getDeviceIds(); ArrayList<WebSocketSession> sessionList = new ArrayList<>(); //存储需要替换模板的session ArrayList<WebSocketSession> necessaryreplacesessionList = new ArrayList<>(); deviceIds.forEach((deviceId) -> { taskDeviceInfo.setId(SequenceUtil.makeStringId()); taskDeviceInfo.setDeviceinfoid(Integer.valueOf(deviceId)); taskDeviceInfo.setTaskid(task.getId()); taskDeviceInfo.setCreatetime(new Date()); taskDeviceInfo.setUpdatetime(new Date()); //入库 taskDeviceInfoService.insertTaskDeviceInfo(taskDeviceInfo); //4.拿到设备序列号,通过序列号获取到对应的session DeviceInfo deviceInfo = deviceInfoService.selectDeviceInfoById(Long.valueOf(deviceId)); String serial = deviceInfo.getSerial(); WebSocketSession socketSession = (WebSocketSession) RealTimeHandler.SESSION_POOL.get(serial); sessionList.add(socketSession); //关联deviceInfo & Template //查出所有用户选择需要替换模板的信息deviceUseTemplateIds ArrayList<String> deviceUseTemplateIds = caseExecute.getDeviceUseTemplateIds(); if (deviceUseTemplateIds.contains(deviceId)){ //需要替换模板session necessaryreplacesessionList.add(socketSession); //判断是否有空闲合适模板替换,如果有就替换,没有就不替换 if (templates == null || templates.size() < 0) { log.info("hasAvailableTemplate not find template login.phone,login.pwd, do nothing"); } // 查出所有可用的模板手机号,密码 List<String> Ids = templates.stream().map(Template::getId).filter(Objects::nonNull).collect(Collectors.toList()); Ids.removeAll(alreadyIds); List<String> values = templates.stream().map(Template::getValue).filter(Objects::nonNull).collect(Collectors.toList()); values.removeAll(alreadyValues); if (values.size() > 0) { //有未被占用的手机号 String value = values.get(0); alreadyValues.add(value); String id1=Ids.get(0); alreadyIds.add(id1); //devicesId 在deviceUseTemplateIds中,需要关联“设备模板” DeviceInfoTemplate deviceInfoTemplate = new DeviceInfoTemplate(); deviceInfoTemplate.setId(SequenceUtil.makeStringId()); deviceInfoTemplate.setTemplateid(id1); deviceInfoTemplate.setDeviceInfoid(deviceId); deviceInfoTemplate.setCreatetime(new Date()); deviceInfoTemplate.setUpdatetime(new Date()); //入库 deviceInfoTemplateService.insertDeviceInfoTemplate(deviceInfoTemplate); //devicesId 在deviceUseTemplateIds中,需要关联“任务模板” TaskTemplate taskTemplate = new TaskTemplate(); taskTemplate.setId(SequenceUtil.makeStringId()); taskTemplate.setTemplateid(id1); taskTemplate.setTaskid(task.getId()); taskTemplate.setCreatetime(new Date()); taskTemplate.setUpdatetime(new Date()); //入库 taskTemplateService.insertTaskTemplate(taskTemplate); } else { //模板没有足够手机号|密码 log.info("No template available, do nothing"); } } }); //5.task以及关联表入库 taskService.insertTask(task); //6.下发用例到solopi sessionList.forEach((session) -> { try { CaseExecuteDto caseExecuteDto = new CaseExecuteDto(); ArrayList<String> caseInfoList = new ArrayList<>(); //需要替换的用例List caseExecute.getCaseIds().forEach((caseId) -> { CaseInfo caseInfo = caseInfoService.selectCaseInfoById(Long.valueOf(caseId)); if (caseInfo != null) { String currentCaseInfo = caseInfo.getCaseinfo(); if (StringUtils.isNotBlank(currentCaseInfo) && (currentCaseInfo.contains("login.phone") ||currentCaseInfo.contains("login.pwd")) && necessaryreplacesessionList.contains(session)) { currentCaseInfo = convertCaseInfo(currentCaseInfo); } caseInfoList.add(currentCaseInfo); } }); caseExecuteDto.setCaseInfoList(caseInfoList); caseExecuteDto.setTaskId(task.getId()); HashMap<String, CaseExecuteDto> caseInfoExecuteHashMap = new HashMap<>(); caseInfoExecuteHashMap.put("execCase", caseExecuteDto); String json = new Gson().toJson(caseInfoExecuteHashMap); session.sendMessage(new TextMessage(json)); } catch (IOException e) { e.printStackTrace(); } }); alreadyValue.clear(); return 0; }
下发后,solopi进行执行,执行完成后上传测试报告。
核心代码:
接收客户端上传的报告信息,以任务id为唯一标识,对任务数据进行储存。
else if (s.contains("replayResultInfo")) { /*GsonBuilder builder = new GsonBuilder(); builder.registerTypeAdapter(Date.class, new JsonDeserializer<Date>() { public Date deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException { return new Date(json.getAsJsonPrimitive().getAsLong()); } }); Gson gson = builder.create(); List<ReplayResult> mResults=gson.fromJson(messageMap.get(s).toString(), new TypeToken<List<ReplayResult>>() { }.getType()); int totalNum = 0; int successNum = 0; for (ReplayResult bean : mResults) { totalNum++; if (StringUtil.isEmpty(bean.getExceptionMessage())) { successNum++; } }*/ //TODO:映射为报告对象,不要做字符串截取 String taskId = s.substring(16); Task task = taskService.selectTaskById(taskId);//通过taskid查询到task对象 //获取当前设备序列号 String currentSerial = SESSIONID_SERIAL_MAPPING.get(session.getId()); log.info("当前设备序列号" + currentSerial); com.alibaba.fastjson.JSONArray jsonArray = JSONObject.parseArray(messageMap.get(s).toString()); //任务存在,代表是下发执行的,进行保存,否则不做处理 if (task != null) { TaskReport taskReport = new TaskReport(); taskReport.setTaskid(taskId); taskReport.setContent(messageMap.get(s).toString()); taskReport.setDeviceinfoserial(currentSerial); //taskReport.setSuccessnum(successNum); //taskReport.setFailnum(totalNum-successNum); for (int i = 0; i < jsonArray.size(); i++) { String caseName = jsonArray.getJSONObject(i).getString("caseName"); taskReport.setCaseName(caseName); taskReportService.insertTaskReport(taskReport); } } }
最终前端报告展示: