作者介绍:胡嘉椿,来自货拉拉/技术中心/质量保障部,专注于移动测试效能方向。
随着手机市场的多元化趋势的持续加剧,App 测试领域所承受的考验愈发严峻,测试人员需要覆盖的品牌、操作系统、系统版本数量呈现爆炸性增长,这无疑极大的增加了测试工作的复杂度和工作量。
泰勒在《科学管理原理》中提出的任务管理法强调了对工作的科学分析和分解,将复杂繁琐的任务划分为多个简单、标准化的子任务。这种任务分解的思路为并行处理提供了可能。通过合理的任务划分和分配,不同的任务或子任务可以由不同的工人或团队并行执行,从而提高整体的工作效率。
从泰勒管理学的任务拆解法中可提取关键信息:子任务、标准化,并行。
我有个大胆的想法...
让 1 = N ? 从 “串行测试” 到 “并行测试”
子任务:三端(Android、iOS、HarmonyOS)的功能测试、兼容性测试都可拆分为不同的子任务;
标准化:功能测试、兼容性测试的覆盖场景为准出条件;
并行:测试人员通过标准化流程并行执行多个子任务;
我们也向一线业务测试团队征集了关于多机同步方案的一些想法,从可落地的角度了解业务测试团队在实际应用中的主要诉求,主要包括如下几个方面:
在移动端测试领域,面对测试量庞大的行业痛点,业界也有不少多机联动方案来提高测试效率,我们选取了具有代表性的一些大厂及测试服务提供商的实现方案做了一个简单调研。
SoloPi 通信层面是通过无线 adb 且不涉及 Web 画面传输,所以理论上联机设备不限量,精细化程度也做到了元素级别的同步。但是 SoloPi 是基于 Android 系统的产品,相较于基于云真机的 Testin 和泽众无法提供跨平台服务,且上手难度较高需要配置各种前置条件。
鉴于以上情况,为了更好地满足货拉拉在多机同步测试方面的需求,我们决定抛弃现有的开源方案,转而自研贴合货拉拉业务测试场景的多机同步工具。我们将充分借鉴 SoloPi 在元素级别同步方面的优势,同时克服其在跨平台兼容性和上手难度上的不足,通过深度定制和优化,期望能够打造出一款既高效又易用、能够全面满足货拉拉多机同步测试需求的解决方案。
结合业界方案的调研与业务上对多机同步使用的诉求,再者,货拉拉在云真机建设的探索与成功实践(感兴趣的小伙伴可以参考我们之前发布的相关文章《货拉拉云真机平台的演进与实践》),我们决定基于货拉拉云真机来实践多机同步的测试。主要因为云真机有以下几个优势:
EventBus:EventBus 是分配消息的一种实现机制,它拥有部署在 Agent 的手机建立的 websokect 长连接的管理及操控权限。消息则是通过 EventBus 的发送到 Agent 的端,最后由 Agent 注入到手机执行消费。
eventBus.emit('syncAppointControl',
{
udId: serial,
action: JSON.stringify(MITT_MESSAGE),
}) //操控消息
eventBus.emit('syncKeyevent', JSON.stringify(message)) //键盘事件
Message:为了降低维护成本并确保未来对 HarmonyOS 的顺利接入,我们在消息传递层面设计了一套统一的协议。这套协议的核心思想是将浏览器层面拦截到的用户操作转换为预定义的协议消息体,然后通过 EventBus 进行分发。不同接入的系统(如 Android、iOS、HarmonyOS)在接收到这些消息后,会根据各自平台的特性进行解析和定制化适配。
import {
ANDROID,
IPHONE,
MITT_MESSAGE,
TOUCH_U,
TOUCH_D,
TOUCH_M,
LONG_PRESS,
SWIPE,
TAP,
KEY_EVENT,
INPUT_EVENT,
SHELL_EVENT,
OPEN_APP,
OPEN_IOSAPP,
STOP_APP,
STOP_IOSAPP,
INSTALL,
UN_REINSTALL,
REINSTALL,
UNINSTALL,
UNLOCK
} from '../Sync/ActionEvent'
实现精准控制,在《货拉拉 App 录制回放的探索与实践》我们有过相关 OCR 的介绍,这里我们不赘述原理,重点是决定复用相关的能力,采用 Paddleocr 来实现元素的精准控制,相对于基于系统 AccessibilityService 和 xcuitest 控制同步,基于 OCR 来实现精准控制有以下几个优点:
Paddleocr 的能力和其提供官方通用模型,对于我们来说,虽然是开箱即用,但是还有一部分移动端的特殊场景检测效果并不理想,比如对于同行相近的文本、边界场景识别结果并不是我们想要的。
相近文本拼接与边界字符的引入
所以我们决定在官方的通用检测模型上进行调优,训练适合货拉拉移动端 UI 页面检测的模型。
解决文本拼接与边界字符问题
针对用户在实际操作过程中可能因点击到文本区域外而导致操作无法正确响应的问题,我们旨在通过扩大文本有效点击区域来提升用户体验。虽然官方提供的 det_db_unclip_ratio 参数可以调整 DB 模型输出框比例,但考虑到其对文本检测准确度产生负面影响,并不推荐直接修改该参数。更为优雅的解决方案是:在文本检测和识别流程完成后,对识别结果进行后处理,通过放大文本框的方式增加有效点击区域。为确保放大后的文本框之间不产生重叠,具体核心在于尝试使用最大的扩展像素值进行放大,并在检测到重叠时逐步减小扩展像素值,直至所有文本框均不再重叠或达到最大迭代次数。以下是该算法的实现步骤:
def is_overlapping(box1, box2):
min_x1, min_y1 = min(pt[0] for pt in box1), min(pt[1] for pt in box1)
max_x1, max_y1 = max(pt[0] for pt in box1), max(pt[1] for pt in box1)
min_x2, min_y2 = min(pt[0] for pt in box2), min(pt[1] for pt in box2)
max_x2, max_y2 = max(pt[0] for pt in box2), max(pt[1] for pt in box2)
return not (min_x1 >= max_x2 or min_x2 >= max_x1 or min_y1 >= max_y2 or min_y2 >= max_y1)
def expand_box(box, expand_pixels):
min_x = min(point[0] for point in box) - expand_pixels
max_x = max(point[0] for point in box) + expand_pixels
min_y = min(point[1] for point in box) - expand_pixels
max_y = max(point[1] for point in box) + expand_pixels
return [[min_x, min_y], [max_x, min_y], [max_x, max_y], [min_x, max_y]]
def adjust_boxes(ocr_results, max_expand_pixels, min_expand_pixels=0, step=1):
original_boxes = [result['bounding_box'] for result in ocr_results]
current_expand = [max_expand_pixels] * len(original_boxes)
def adjust_single_box(idx):
nonlocal current_expand, original_boxes
expanded_boxes = [expand_box(box, current_expand[idx]) for idx, box in enumerate(original_boxes)]
for j in range(len(expanded_boxes)):
if idx != j and is_overlapping(expanded_boxes[idx], expanded_boxes[j]):
if current_expand[idx] > min_expand_pixels:
current_expand[idx] = max(current_expand[idx] - step, min_expand_pixels)
return adjust_single_box(idx)
elif current_expand[j] > min_expand_pixels:
current_expand[j] = max(current_expand[j] - step, min_expand_pixels)
return adjust_single_box(j)
return expanded_boxes, False
iteration = 0
while True:
no_collision = True
for i in range(len(original_boxes)):
expanded_boxes, collision_detected = adjust_single_box(i)
if collision_detected:
no_collision = False
break
if no_collision:
break
iteration += 1
if iteration > 1000:
break
return expanded_boxes
def zoom_in(ocr_results):
max_expand_pixels = 10
return adjust_boxes(ocr_results, max_expand_pixels, min_expand_pixels=1, step=0.5)
调整前效果与扩大后效果如下图所示:
文本区域周围最大偏移 10 个像素,如果出现相邻文本框,动态调整间距,直至矩阵框不交割。
多机同步过程中,若流量开销过大,可能导致区域网络拥堵(目前工区网络的使用已经达到极限)和实际体验的卡顿。解决方案通常涉及优化传输数据及同步策略:比如压缩传输数据,减少不必要的通信。
多机同步中会关闭过渡动画,目前机房大部分设备基本控制在(静置)10kb~(高度渲染)500kb 左右每秒的带宽消耗。
PD2130:/ $ settings put global window_animation_scale 0
PD2130:/ $ settings put global transition_animation_scale 0
PD2130:/ $ settings put global animator_duration_scale 0
在自动化测试过程中,突然出现的弹窗可能会影响测试结果,同样在多机同步测试过程中,如果突然出现弹窗,对用户来讲就意味着增加的额外的操作成本。对于弹窗,我们主要分为两大类:系统弹窗和 App 弹窗。
主要系统的升级提醒,对于华为、荣耀等品牌的手机,我们可以通过禁用系统应用的方式来屏蔽更新
adb shell pm disable-user com.huawei.android.hwouc
adb shell pm disable-user com.hihonor.ouc
adb shell pm disable-user com.meizu.flyme.update
adb shell pm disable-user com.bbk.updater
但对于 iPhone,普遍的方法是通过安装某个过期 Beta 更新描述文件来屏蔽正式版本的更新,但是此方法对于新加入的手机需要经常换描述文件,且每次配置比较繁琐。我们采用在局域网内屏蔽掉苹果的更新域名地址,目前可以说是一劳永逸的方法,以下两个域名屏蔽后,验证下来只会屏蔽掉系统更新服务,不会影响到其它服务。
mesu.apple.com
appldnld.apple.com
主要是 App 向系统的权限申请弹窗,一般是位置定位、网络申请权限、SD 卡申请权限等。Android 各种系统弹窗的碎片化主要是由于 Android 的开放性和多样性,导致不同设备制造商、不同系统版本以及不同定制版本的 Android 系统对弹窗的设计、实现和展示方式存在显著差异。最简单粗暴的方式就是如何屏蔽掉这些,而不是通过脚本去自动化处理,因为随着系统及 ROM 的升级,自动化脚本也需要随之维护
不同品牌、系统的权限弹窗
我们通过运行在手机内拥有 DeviceOwner 权限的天宫管家来控制 App 的权限申请弹窗。关于天宫管家 App,我们之前的文章也有相关的引用,这里我们再简单介绍一下实现方法。
配置相关权限:
<?xml version="1.0" encoding="utf-8"?>
<device-admin xmlns:android="http://schemas.android.com/apk/res/android">
<uses-policies>
<force-lock />
<expire-password />
<encrypted-storage />
<disable-camera />
<limit-password />
<watch-login />
<reset-password />
<wipe-data />
</uses-policies>
</device-admin>
实现相关服务
public class DevAdminReceiver extends DeviceAdminReceiver {
@Override
public void onDisabled(Context context, Intent intent) {
super.onDisabled(context, intent);
}
@Override
public void onNetworkLogsAvailable(Context context, Intent intent, long batchToken, int networkLogsCount) {
super.onNetworkLogsAvailable(context, intent, batchToken, networkLogsCount);
}
@Override
public void onPasswordChanged(Context context, Intent intent, UserHandle userHandle) {
}
@Override
public void onSystemUpdatePending(Context context, Intent intent, long receivedTime) {
super.onSystemUpdatePending(context, intent, receivedTime);
}
@Override
public void onUserAdded(Context context, Intent intent, @NonNull UserHandle newUser) {
super.onUserAdded(context, intent, newUser);
}
}
ComponentName adminComponentName = new ComponentName(context, DevAdminReceiver.class);
DevicePolicyManager devicePolicyManager = (DevicePolicyManager) context.getSystemService(Context.DEVICE_POLICY_SERVICE);
Android 端 5 台同步下单测试演示,可点击链接查看
Android+iOS 同步下单测试演示,可点击链接查看
“道阻且长,行则将至,行而不辍,未来可期”。——《荀子·修身》
面对移动端测试的繁琐性、平台多样性以及覆盖设备广泛的复杂挑战,本次探索是我们从单线程的传统测试模式向多线程、多设备同步测试的一次重要实践与大胆尝试。正如《荀子·修身》所言:“道阻且长,行则将至,行而不辍,未来可期”。我们坚信,通过持续的技术创新与实践努力,依然可以不断提升测试效率与质量。