Aone.Net

学无止境
AI-移动端 APP / 游戏 APP UI 自动化断言方案

移动端比 Web 复杂得多:原生控件树(APP)vs Canvas 渲染(游戏),断言方案完全不同。


一、先分清两大场景

 普通 APP(原生/混合)游戏 APP(Unity/Cocos/UE)
渲染方式 原生控件 / WebView Canvas / OpenGL 直接画图
能否获取控件树 ✅ 可以(UIAutomator2/XCUITest) ❌ 不行(没有DOM)
核心断言手段 控件属性 + 图像识别 图像识别为主 + 引擎Hook
代表工具 Appium, uiautomator2, Espresso Airtest, Maate, UnityTest

二、普通移动 APP 断言方案

方案 1:控件属性断言(UIAutomator2 / Appium)

python
1# uiautomator2(Android 推荐)
2import uiautomator2 as u2
3
4d = u2.connect()
5
6# ① 文本断言
7assert d(text="登录").exists, "登录按钮不存在"
8assert d(text="欢迎回来").get_text() == "欢迎回来", "欢迎语错误"
9
10# ② 状态断言
11assert d(resourceId="com.app:id/checkbox").is_checked(), "复选框未选中"
12assert d(resourceId="com.app:id/btn").is_enabled(), "按钮不可点击"
13
14# ③ 属性断言
15el = d(resourceId="com.app:id/avatar")
16assert el.info['contentDescription'] == "用户头像"
17assert el.info['bounds']  # 位置大小是否合理
18
19# ④ 列表断言
20items = d(resourceId="com.app:id/list").children()
21assert len(items) == 10, f"列表期望10项,实际{len(items)}项"
22assert items[0].get_text() == "第一条数据"
23
24# ⑤ 层级结构断言
25assert d(resourceId="com.app:id/parent").child(text="子按钮").exists
26

方案 2:图像识别断言(Airtest 模板匹配)

当控件无法定位(自定义View、游戏化UI、WebView)时用

python
1from airtest.core.api import *
2
3# ① 模板匹配 - 判断元素是否存在
4# 存在(相似度>0.9) → 通过;不存在 → 抛出TargetNotFoundError
5assert_exists(Template("tpl_login_btn.png", threshold=0.9), 
6              "登录按钮未找到")
7
8# ② 模板匹配 - 判断元素状态(按钮变灰/变亮)
9# 灰色按钮模板
10if exists(Template("tpl_btn_disabled.png", threshold=0.8)):
11    assert False, "按钮被禁用了,不应出现"
12
13# ③ 区域截图对比 - 验证整个页面
14assert_snapshot(Template("page_home.png", threshold=0.95), 
15                "首页截图不匹配")
16
17# ④ OCR 文字断言
18from airtest.core.api import Ocr
19ocr = Ocr()
20text = ocr.ocr("screen.png")
21assert "订单编号" in text, "页面缺少'订单编号'文字"
22assert "¥128.00" in text, "价格显示错误"
23

方案 3:混合方案(控件 + 图像互补)

python
1# 先用控件定位容器,再用图像验证内部内容
2container = d(resourceId="com.app:id/banner")
3banner_img = container.screenshot()
4assert_exists(Template("tpl_banner_v2.png", threshold=0.9), 
5              "Banner图片不是最新版本")
6
7# 控件定位 + OCR 验证文字
8price_el = d(resourceId="com.app:id/price")
9assert Ocr.ocr(price_el.screenshot()) == "¥99.00"
10

三、🎮 移动端游戏 APP 断言方案(重点)

游戏没有控件树!所有断言几乎都依赖图像识别 + 特殊手段

⭐ 方案 1:Airtest 模板匹配(最主流)

python
1from airtest.core.api import *
2
3# ① 基础 - 判断游戏元素是否出现
4assert_exists(Template("tpl_hp_bar.png"), "血条未显示")
5
6# ② 判断数值变化(金币从 100 → 200)
7# 先OCR识别当前金币
8gold_text = Ocr.ocr(Template("tpl_gold_area.png"))
9assert int(gold_text) == 200, f"金币应为200,实际{gold_text}"
10
11# ③ 判断按钮状态(可点击/灰色/红点提示)
12if exists(Template("tpl_btn_with_reddot.png", threshold=0.8)):
13    touch(Template("tpl_btn_with_reddot.png"))
14    assert not exists(Template("tpl_btn_with_reddot.png")), "红点未消失"
15
16# ④ 判断弹窗出现
17wait(Template("tpl_dialog_victory.png"), timeout=10)
18assert_exists(Template("tpl_dialog_victory.png"), "胜利弹窗未出现")
19
20# ⑤ 判断场景切换(通过标志性图片)
21assert_exists(Template("tpl_stage_3_bg.png"), "未进入第3关")
22

⭐ 方案 2:图像特征点匹配(SIFT/SURF — 抗旋转缩放)

python
1# 模板匹配怕旋转/缩放?用特征点匹配
2from airtest.core.api import features_match, ST
3
4# 在游戏画面中找"开始游戏"按钮(可能有旋转/缩放)
5pos = features_match(Template("tpl_start_btn.png"))
6if pos:
7    touch(pos)
8else:
9    assert False, "开始按钮找不到(可能被遮挡/旋转)"
10

⭐ 方案 3:区域颜色/像素断言(轻量但有效)

python
1import cv2
2import numpy as np
3from airtest.core.api import load_image
4
5img = load_image("screen.png")
6
7# ① 判断某区域颜色(比如血条是否变红=低血量)
8hp_region = img[100:120, 200:300]  # 裁剪血条区域
9avg_color = np.mean(hp_region, axis=(0,1))
10# 红色通道高 = 血量低
11assert avg_color[2] > 200, "血量应该很低(红色)"
12
13# ② 判断某像素点(比如右上角小地图是否有敌人红点)
14pixel = img[50, 350]
15assert pixel[2] > 200, "小地图应有敌人红点"
16
17# ③ 判断画面是否黑屏/闪退
18assert np.mean(img) > 10, "画面全黑,可能闪退"
19

⭐ 方案 4:游戏引擎 Hook 断言(最精准)

Unity 游戏

python
1# 通过 adb + Unity C# 反射获取内部数据
2# 需要游戏有开启 "Development Build" + "Script Debugging"
3
4# 方案A: adb shell 调用 Unity 内部方法
5import subprocess
6
7def get_unity_player_hp():
8    result = subprocess.run(
9        ["adb", "shell", "am", "broadcast", 
10         "-a", "com.game.GET_HP", "--es", "key", "hp"],
11        capture_output=True, text=True
12    )
13    hp = int(result.stdout.strip())
14    assert hp > 0, f"玩家血量异常: {hp}"
15    return hp
16
17# 方案B: 使用 Maate 框架(专为Unity游戏测试)
18from maate.unity import Unity
19unity = Unity()
20hp = unity.get_player_hp()
21assert hp == 100, f"初始血量应为100,实际{hp}"
22
23# 方案C: 读取内存(需要root/越狱)
24def read_game_memory(address):
25    # /proc/pid/maps + /dev/mem 或 frida hook
26    pass
27

Cocos2d-x 游戏

python
1# Cocos 游戏可以通过 JS Bridge(如果内嵌JS)或 hook native层
2# 常用: frida hook Cocos2d-x 的 getChildByTag / getPosition
3
4import frida
5
6script = """
7Java.perform(function() {
8    var Player = Java.use("org.cocos2dx.cpp.Player");
9    Player.getHp.implementation = function() {
10        var hp = this.getHp();
11        console.log("[TEST] HP = " + hp);
12        return hp;
13    };
14});
15"""
16# 注入后读取日志获取HP
17

⭐ 方案 5:ADB 命令辅助断言

python
1import subprocess
2
3# ① 判断页面是否在前台(防止切到后台)
4def is_app_foreground(package):
5    result = subprocess.run(
6        ["adb", "shell", "dumpsys", "window", "windows"],
7        capture_output=True, text=True
8    )
9    assert package in result.stdout, f"{package} 不在前台"
10
11# ② 判断帧率(需要游戏开启性能监控)
12def get_fps():
13    result = subprocess.run(
14        ["adb", "shell", "dumpsys", "gfxinfo", "com.game"],
15        capture_output=True, text=True
16    )
17    # 解析 Janky frames 数量
18    assert "0 janky" in result.stdout, "游戏掉帧了"
19
20# ③ 判断内存
21def get_memory(package):
22    result = subprocess.run(
23        ["adb", "shell", "dumpsys", "meminfo", package],
24        capture_output=True, text=True
25    )
26    assert "512 MB" not in result.stdout, "内存占用过高"
27
28# ④ 判断是否ANR/Crash
29def check_crash():
30    result = subprocess.run(
31        ["adb", "logcat", "-d", "*:E"],
32        capture_output=True, text=True
33    )
34    assert "FATAL EXCEPTION" not in result.stdout, "游戏崩溃了"
35    assert "ANR" not in result.stdout, "应用无响应"
36

⭐ 方案 6:游戏特有逻辑断言

python
1# ① 战斗结果断言
2def assert_battle_result(expected_win=True):
3    wait(Template("tpl_battle_result.png"), timeout=15)
4    result_text = Ocr.ocr(Template("tpl_result_text_area.png"))
5    if expected_win:
6        assert "胜利" in result_text or "VICTORY" in result_text
7    else:
8        assert "失败" in result_text or "DEFEAT" in result_text
9
10# ② 关卡进度断言
11def assert_stage(expected_stage):
12    stage_text = Ocr.ocr(Template("tpl_stage_indicator.png"))
13    assert f"Stage {expected_stage}" in stage_text
14
15# ③ 抽奖/抽卡结果断言(随机但可验证概率分布)
16def assert_gacha_result(expected_rarity="SSR"):
17    wait(Template("tpl_gacha_result.png"), timeout=10)
18    rarity = Ocr.ocr(Template("tpl_rarity_text.png"))
19    assert rarity == expected_rarity, f"期望{expected_rarity},实际{rarity}"
20
21# ④ 连续操作后的状态断言
22def assert_after_combo():
23    touch(Template("tpl_attack_btn.png"))
24    touch(Template("tpl_attack_btn.png"))
25    touch(Template("tpl_attack_btn.png"))
26    # 连击3次后应有特效
27    assert_exists(Template("tpl_combo_x3_effect.png"), "连击特效未显示")
28    assert_exists(Template("tpl_damage_number.png"), "伤害数字未显示")
29

四、📊 性能断言(APP 和游戏都需要)

指标工具断言示例
FPS adb dumpsys gfxinfo assert fps > 55
内存 adb dumpsys meminfo assert mem < 512MB
CPU adb shell top assert cpu < 80%
温度 adb shell cat /sys/... assert temp < 45°C
启动时间 adb shell am start assert start_time < 3s
流量 adb shell cat /proc/net/dev assert traffic < 10MB
电量 adb shell dumpsys battery assert battery > 20%
python
1# 性能断言示例
2def assert_performance(fps_min=50, mem_max_mb=512, cpu_max=80):
3    # FPS
4    gfx = subprocess.run(
5        ["adb", "shell", "dumpsys", "gfxinfo", "com.game"],
6        capture_output=True, text=True
7    ).stdout
8    fps = int(gfx.split("Frames:")[1].split()[0])
9    assert fps >= fps_min, f"FPS={fps} 低于阈值{fps_min}"
10    
11    # 内存
12    mem = subprocess.run(
13        ["adb", "shell", "dumpsys", "meminfo", "com.game"],
14        capture_output=True, text=True
15    ).stdout
16    mem_mb = int(mem.split("Private Dirty:")[1].split("MB")[0])
17    assert mem_mb <= mem_max_mb, f"内存={mem_mb}MB 超过{mem_max_mb}MB"
18

五、工具选型速查表

场景首选工具断言核心能力
普通 APP(Android) uiautomator2 + Airtest 控件树 + 模板匹配
普通 APP(iOS) XCUITest + Airtest 控件树 + 模板匹配
混合 APP / H5 Appium + Airtest WebView控件 + 截图OCR
游戏 APP(通用) AirtestIDE/Project 模板匹配 + OCR + 特征点
Unity 游戏 Airtest + Maate 图像 + 引擎Hook
Cocos 游戏 Airtest + frida 图像 + native hook
性能断言 adb命令 + PerfDog FPS/内存/CPU
跨平台 Airtest + uiautomator2 一套代码通吃

六、最佳实践总结

 
1普通APP断言优先级:
2  控件属性断言(快、稳) 
3    → 失败时降级为 图像识别断言(慢、但通用)
4      → 再失败用 OCR 文字断言
5
6游戏APP断言优先级:
7  图像模板匹配(90%场景够用)
8    → 旋转/缩放用 特征点匹配
9      → 需要精确数值用 引擎Hook / 内存读取
10        → 性能用 adb 命令兜底
11
12终极兜底:
13  截图对比(assert_snapshot)—— 任何场景都能用,就是慢
14

一句话总结:普通 APP 靠控件树,游戏 APP 靠图像识别 + 引擎 Hook,性能靠 adb,万不得已用截图对比兜底。

posted on 2026-05-09 14:23  Catonce  阅读(75)  评论(0)    收藏  举报