上一篇介绍了在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自动化实战【下】的更多相关文章

  1. 融云技术分享:融云安卓端IM产品的网络链路保活技术实践

    本文来自融云技术团队原创分享,原文发布于“ 融云全球互联网通信云”公众号,原题<IM 即时通讯之链路保活>,即时通讯网收录时有部分改动. 1.引言 众所周知,IM 即时通讯是一项对即时性要 ...

  2. 《Selenium+Pytest Web自动化实战》随到随学在线课程,零基础也能学!

    课程介绍 课程主题:<Selenium+Pytest Web自动化实战> 适合人群: 1.功能测试转型自动化测试 2.web自动化零基础的小白 3.对python 和 selenium 有 ...

  3. Selenium2学习-039-WebUI自动化实战实例-文件上传下载

    通常在 WebUI 自动化测试过程中必然会涉及到文件上传的自动化测试需求,而开发在进行相应的技术实现是不同的,粗略可划分为两类:input标签类(类型为file)和非input标签类(例如:div.a ...

  4. Selenium2学习-014-WebUI自动化实战实例-012-Selenium 操作下拉列表实例-div+{js|jquery}

    之前已经讲过了 Selenium 操作 Select 实现的下拉列表:Selenium2学习-010-WebUI自动化实战实例-008-Selenium 操作下拉列表实例-Select,但是在实际的日 ...

  5. 安卓端通过http对Mysql进行增删改查

    各类it学习视频,大家都可以看看哦!我自己本人都是通过这些来学习it只知识的! 下面是视频链接转自:http://www.cnblogs.com/yzxk/p/4749440.html Android ...

  6. H5安卓端浏览器如何去除select的边框?

    H5安卓端浏览器如何去除select的边框? android下没有问题,在apple下无三角号. -webkit-appearance:none; border-radius:0

  7. 关于微信内置浏览器安卓端session丢失问题

    项目上线测试,发现微信安卓端存在用户登录无法验证session情况, 导致每次接口请求都无法识别,而苹果客户端不会出现此问题,非微信环境打开不会出现此问题,找到一些解决方案做下记录: 方案1: 由于微 ...

  8. React-Native App启动页制作(安卓端)

    原文地址:React-Native App启动页制作(安卓端) 这篇文章是根据开源项目react-native-splash-screen来写的.在使用react-native-link命令安装该包后 ...

  9. 第9期《jmeter接口自动化实战》零基础入门!

    2019年 第9期<jmeter接口自动化实战>课程,12月6号开学! 上课方式:QQ群视频在线教学 本期上课时间:12月6号-1月18号,每周五.周六晚上20:00-22:00 报名费: ...

  10. 如何搭建一个WEB服务器项目(四)—— 实现安卓端图片加载

    使用Glide安卓图片加载库 观前提示:本系列文章有关服务器以及后端程序这些概念,我写的全是自己的理解,并不一定正确,希望不要误人子弟.欢迎各位大佬来评论区提出问题或者是指出错误,分享宝贵经验.先谢谢 ...

随机推荐

  1. FastAPI依赖注入:链式调用与多级参数传递

    title: FastAPI依赖注入:链式调用与多级参数传递 date: 2025/04/05 18:43:12 updated: 2025/04/05 18:43:12 author: cmdrag ...

  2. exe4j工具使用-jar包转exe可执行文件

    exe4j介绍 exe4j可以将java打包的jar包转为exe可执行文件,实现在没有jdk环境下运行jar包. 下载链接 https://pan.baidu.com/s/1sfEJyxPABmhsl ...

  3. Spring解决创建单例bean,而存在线程不安全问题,的解决方案

    一.线程安全问题都是由全局变量.静态变量和类的成员变量引起的.若每个线程中对全局变量.静态变量和类的成员变量只有读操作,而无写 操作,一般来说,这个全局变量是线程安全的,反之线程存在问题 二.因为Sp ...

  4. 🎀Java-Exception与RuntimeException

    简介 Exception Exception 类是所有非致命性异常的基类.这些异常通常是由于编程逻辑问题或外部因素(如文件不存在.网络连接失败等)导致的,可以通过适当的编程手段来恢复或处理.Excep ...

  5. Visual Studio 2022 v17.13新版发布:强化稳定性和安全,助力 .NET 开发提效!

    前言 今天大姚带领大家一起来看看 Visual Studio 2022 v17.13 新版发布都更新了哪些新功能,为我们开发工作带来了哪些便利,是否真的值得我们花费时间把 Visual Studio ...

  6. 一个开源的 Blazor 跨平台入门级实战项目

    前言 今天大姚给大家分享一个开源(MIT license).免费的 Blazor 跨平台入门级实战项目:YourWeather. 项目介绍 YourWeather是一个开源(MIT license). ...

  7. DPDI online在线调度系统环境部署

    DPDI online简介: DPDI Online 是一款基于Kettle的强大在线任务调度平台,凭借其高效与灵活性,专为调度和监控Kettle客户端生成的ETL任务而设计 DPDI online部 ...

  8. 【解决方法】edge浏览器不小心删除收藏夹怎么办?

    C:\Users\用户名\AppData\Local\Microsoft\Edge\User Data\Default 进入该目录,找到名为Bookmarks或Bookmarks.bak或Bookma ...

  9. DP学习总结

    动态规划是一种通过把原问题分解为相对简单的子问题的方式求解复杂问题的方法. -----OI Wiki 例.1-最大子段和 分析 DP四步 ⑴定义状态 定义\(dp_i\)表示以\(i\)结尾的最大子段 ...

  10. SQL Server 2025 中的改进

    SQL Server 2025 中的改进 当我们接近 SQL Server 2025 的首次公开版本时,开始深入探究 Azure SQL DB 如今(已公布和未公布)但在 SQL Server 盒装产 ...