安卓端-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);
}
}
}
最终前端报告展示:

安卓端-APPUI自动化实战【下】的更多相关文章
- 融云技术分享:融云安卓端IM产品的网络链路保活技术实践
本文来自融云技术团队原创分享,原文发布于“ 融云全球互联网通信云”公众号,原题<IM 即时通讯之链路保活>,即时通讯网收录时有部分改动. 1.引言 众所周知,IM 即时通讯是一项对即时性要 ...
- 《Selenium+Pytest Web自动化实战》随到随学在线课程,零基础也能学!
课程介绍 课程主题:<Selenium+Pytest Web自动化实战> 适合人群: 1.功能测试转型自动化测试 2.web自动化零基础的小白 3.对python 和 selenium 有 ...
- Selenium2学习-039-WebUI自动化实战实例-文件上传下载
通常在 WebUI 自动化测试过程中必然会涉及到文件上传的自动化测试需求,而开发在进行相应的技术实现是不同的,粗略可划分为两类:input标签类(类型为file)和非input标签类(例如:div.a ...
- Selenium2学习-014-WebUI自动化实战实例-012-Selenium 操作下拉列表实例-div+{js|jquery}
之前已经讲过了 Selenium 操作 Select 实现的下拉列表:Selenium2学习-010-WebUI自动化实战实例-008-Selenium 操作下拉列表实例-Select,但是在实际的日 ...
- 安卓端通过http对Mysql进行增删改查
各类it学习视频,大家都可以看看哦!我自己本人都是通过这些来学习it只知识的! 下面是视频链接转自:http://www.cnblogs.com/yzxk/p/4749440.html Android ...
- H5安卓端浏览器如何去除select的边框?
H5安卓端浏览器如何去除select的边框? android下没有问题,在apple下无三角号. -webkit-appearance:none; border-radius:0
- 关于微信内置浏览器安卓端session丢失问题
项目上线测试,发现微信安卓端存在用户登录无法验证session情况, 导致每次接口请求都无法识别,而苹果客户端不会出现此问题,非微信环境打开不会出现此问题,找到一些解决方案做下记录: 方案1: 由于微 ...
- React-Native App启动页制作(安卓端)
原文地址:React-Native App启动页制作(安卓端) 这篇文章是根据开源项目react-native-splash-screen来写的.在使用react-native-link命令安装该包后 ...
- 第9期《jmeter接口自动化实战》零基础入门!
2019年 第9期<jmeter接口自动化实战>课程,12月6号开学! 上课方式:QQ群视频在线教学 本期上课时间:12月6号-1月18号,每周五.周六晚上20:00-22:00 报名费: ...
- 如何搭建一个WEB服务器项目(四)—— 实现安卓端图片加载
使用Glide安卓图片加载库 观前提示:本系列文章有关服务器以及后端程序这些概念,我写的全是自己的理解,并不一定正确,希望不要误人子弟.欢迎各位大佬来评论区提出问题或者是指出错误,分享宝贵经验.先谢谢 ...
随机推荐
- Web前端入门第 23 问:CSS 选择器的优先级
任何地方都存在阶级,CSS 选择器也不例外,也会讲一个三六九等. 选择器类别 通配符选择器 标签选择器 类选择器 ID选择器 属性选择器 伪类选择器 伪元素选择器 关系选择器 流传已久的阶级划分 选择 ...
- 第五届新型功能材料国际会议(ICNFM 2025)
第五届新型功能材料国际会议(ICNFM 2025) 2025年5月16日-17日 曼谷,泰国 http://www.icnfm.net/ 会议简介 第五届新型功能材料国际会议(ICNFM 2025)将 ...
- Python科学计算系列12—积分变换
1.拉普拉斯变换及逆变换 拉普拉斯变换公式 拉普拉斯逆变换公式 例子: 代码如下: from sympy import * from sympy.integrals import laplace_tr ...
- java基础之Stream流
一.使用Stream的目的:用于解决已有集合类库既有的弊端,只求关注[目的],不关注[方式],且其数据源:可以是集合,数组等 例子: public class NormalFilter { publi ...
- windows10环境下,remote wsl链接wsl ubuntu使用ubuntu 开发环境,报错:VS Code server for WSL closed unexpectedly check WSL terminal for more details
报错提示这样 在windows的vscode里面的关键报错信息是:vscode wsl Unable to detect if server is already installed: Error: ...
- 解决微信二维码接口接口返回:errcode\":47001,\"errmsg\":\"data format error rid: xxx和处理返回的buffer的问题
data format error rid问题: 在php中使用curl调用微信二维码生成接口getwxacodeunlimit时得到错误响应信息: errcode\":47001,\&qu ...
- Weblogic远程代码执行CVE-2023-21839复现及修复
声明:本文分享的安全工具和项目均来源于网络,仅供安全研究与学习之用, 如用于其他用途,由使用者承担全部法律及连带责任,与工具作者和本公众号无关. WebLogic 存在远程代码执行漏洞(CVE ...
- 47.9K star!全平台开源笔记神器,隐私安全首选!
嗨,大家好,我是小华同学,关注我们获得"最新.最全.最优质"开源项目和高效工作学习方法 "Joplin 是一款开源的笔记记录和待办事项应用,支持端到端加密同步,完美替代商 ...
- dbeaver导入sql脚本报错的排查—— ERROR 1366 (HY000) at line
描述 在使用dbeaver进行sql脚本导入的时候报了以下的错误. C:\Users\xxxx\AppData\Roaming\DBeaverData\drivers\clients\mysql_8\ ...
- 【经验】Git|Windows下如何管理和部署多个Git账号的SSH密钥文件
生成 SSH 密钥 先打开一个git窗口,生成ssh密钥. 如果打开的不是git窗口,而是cmd窗口,则需要先切换到C:\Users\用户名\.ssh目录下. 下面这条指令的your_email和yo ...