游戏测试专题第七篇:游戏自动化测试实战

游戏画面是动态的、UI是自绘的、引擎是多样的------游戏自动化比软件自动化难得多。本文用Airtest+Poco,手把手搭建一套可落地的游戏自动化框架。


目录

  1. 游戏自动化为什么难
  2. 游戏自动化技术方案选型
  3. Airtest+Poco实战
  4. 图像识别脚本编写
  5. 控件识别脚本编写
  6. 搭建游戏自动化框架
  7. 多设备并行执行
  8. 自动化接入CI/CD
  9. 游戏自动化的边界
  10. 总结

一、游戏自动化为什么难

1.1 游戏自动化的四大难点

复制代码
难点1:UI是"画"出来的
  软件:UI是标准控件(按钮/输入框),有DOM/XML结构
  游戏:UI是引擎渲染的,没有标准控件树
  → 传统控件定位方式失效

难点2:画面是动态的
  软件:页面相对静态,元素位置固定
  游戏:角色在动、特效在飞、镜头在转
  → 图像识别容易误匹配

难点3:引擎多样
  Unity / UE / Cocos / 自研引擎
  每种引擎的自动化方案都不同

难点4:自动化本身影响性能
  软件:自动化几乎不影响被测应用
  游戏:截图/识别本身会占资源,影响帧率
  → 可能测出的性能数据失真

1.2 游戏自动化 vs 软件自动化

维度 软件自动化 游戏自动化
定位方式 元素定位(ID/XPath) 图像/控件识别
稳定性 较低(画面变化大)
开发成本
维护成本
适用场景 稳定功能回归 固定流程回归
主流工具 Selenium/Appium Airtest/Poco

二、游戏自动化技术方案选型

2.1 方案对比

方案 原理 优点 缺点
图像识别 截图匹配 通用性强,任何游戏可用 慢、易误匹配
控件识别(Poco) 引擎SDK暴露控件树 精准、快 需接入SDK
内存读取 读游戏内存 数据精准 违法风险、版本敏感
协议模拟 直接发协议包 快、稳定 需破解协议
引擎内置 引擎自带测试工具 深度 依赖引擎

2.2 推荐组合方案

复制代码
最佳实践:图像识别 + 控件识别 组合

Airtest(图像识别):
  适合:动态画面、无控件树的场景
  用途:点击游戏内按钮、验证画面元素

Poco(控件识别):
  适合:有控件树的场景(Unity/UE/Cocos)
  用途:精准定位UI控件,读取属性

组合使用:
  能用控件识别就用控件(快且准)
  控件识别不了的用图像识别兜底

三、Airtest+Poco实战

3.1 环境搭建

bash

复制代码
# 安装Airtest
pip install airtest

# 安装Poco(控件识别)
pip install pocoui

# 下载AirtestIDE(可视化IDE,强烈推荐)
# 官网:https://airtest.netease.com/

3.2 Poco接入游戏

复制代码
Poco接入流程(以Unity为例):

1. 导入Poco SDK到游戏工程
   下载 poco-sdk-unity,导入Unity项目

2. 在游戏场景中挂载Poco脚本
   将 PocoManager 预制体拖入场景

3. 游戏打包
   打一个带Poco的测试包

4. AirtestIDE连接
   连接设备 → 选择Poco辅助窗 → 选择Unity

5. 查看控件树
   在IDE中即可看到游戏UI的控件树

3.3 第一个自动化脚本

python

复制代码
# game_auto.py
"""
游戏自动化测试示例
以登录并进入主界面为例
"""
from airtest.core.api import *
from poco.drivers.unity3d import UnityPoco
import time

# 连接设备
auto_setup(__file__)

# 初始化Poco(Unity引擎)
poco = UnityPoco()


def test_login_and_enter_game():
    """登录并进入游戏主界面"""
    
    # 1. 等待登录界面出现
    poco(text="登录").wait_for_appearance(timeout=30)
    print("登录界面已加载")
    
    # 2. 输入账号密码
    poco("LoginPanel").child("AccountInput").set_text("test_account")
    poco("LoginPanel").child("PasswordInput").set_text("test_password")
    
    # 3. 点击登录按钮
    poco("LoginPanel").child("LoginBtn").click()
    
    # 4. 等待主界面出现
    poco("MainPanel").wait_for_appearance(timeout=30)
    print("已进入主界面")
    
    # 5. 验证主界面元素
    assert poco("MainPanel").exists(), "主界面未出现"
    assert poco("PlayerLevel").exists(), "等级显示缺失"
    print("主界面元素验证通过")


if __name__ == '__main__':
    test_login_and_enter_game()

四、图像识别脚本编写

4.1 图像识别原理

复制代码
图像识别流程:
  截图 → 与模板图片匹配 → 找到相似度最高的位置 → 返回坐标

关键参数:
  threshold:相似度阈值(0-1,默认0.7,越高越严格)
  target_pos:点击位置(1-9九宫格,默认5即中心)
  rgb:是否用RGB匹配(默认False用灰度)

4.2 图像识别脚本示例

python

复制代码
# image_auto.py
"""
基于图像识别的游戏自动化
适合无控件树的场景
"""
from airtest.core.api import *

auto_setup(__file__)


def test_ui_by_image():
    """用图像识别测试游戏UI"""
    
    # 1. 等待并点击"开始游戏"按钮
    # Template图片需提前截好放在同级目录
    touch(Template(r"tpl_start_game.png", threshold=0.8))
    sleep(2)
    
    # 2. 等待加载完成
    wait(Template(r"tpl_main_ui.png", threshold=0.8), timeout=30)
    
    # 3. 点击"背包"图标
    touch(Template(r"tpl_bag_icon.png", threshold=0.8))
    sleep(1)
    
    # 4. 验证背包面板出现
    assert exists(Template(r"tpl_bag_panel.png", threshold=0.8)), "背包未打开"
    
    # 5. 关闭背包
    touch(Template(r"tpl_close_btn.png", threshold=0.8))


def test_combat_flow():
    """测试战斗流程"""
    
    # 进入战斗
    touch(Template(r"tpl_battle_entry.png", threshold=0.8))
    sleep(3)
    
    # 释放技能(点击技能按钮)
    touch(Template(r"tpl_skill_1.png", threshold=0.7))
    sleep(1)
    touch(Template(r"tpl_skill_2.png", threshold=0.7))
    sleep(1)
    
    # 等待战斗结算
    wait(Template(r"tpl_battle_result.png", threshold=0.8), timeout=60)
    print("战斗流程完成")


if __name__ == '__main__':
    test_ui_by_image()

4.3 图像识别提升稳定性技巧


五、控件识别脚本编写

5.1 控件定位方式

python

复制代码
# Poco控件定位的多种方式

# 1. 按控件名
poco("LoginBtn").click()

# 2. 按文本
poco(text="登录").click()

# 3. 按层级(父子关系)
poco("MainPanel").child("TopBar").child("SettingsBtn").click()

# 4. 按属性
poco("Item").attr("type")  # 获取属性
poco(type="Button").click()

# 5. 按索引(同名控件的第几个)
poco("Item")[0].click()      # 第一个
poco("Item")[-1].click()     # 最后一个

# 6. 组合定位
poco(text="确定", type="Button").click()

5.2 控件操作大全

python

复制代码
# 基础操作
poco("Btn").click()                    # 点击
poco("Input").set_text("hello")        # 设置文本
poco("Input").get_text()               # 获取文本
poco("Text").get_text()                # 获取显示文本

# 属性获取
poco("Btn").attr("visible")            # 是否可见
poco("Btn").attr("pos")                # 位置
poco("Image").attr("sprite")           # 图片资源名

# 滑动操作
poco("List").swipe('up')               # 向上滑动
poco("List").swipe([0.5, 0.5], [0.5, 0.1])  # 自定义方向

# 等待
poco("Panel").wait_for_appearance(timeout=10)   # 等待出现
poco("Panel").wait_for_disappearance(timeout=10) # 等待消失

# 断言
assert poco("Panel").exists()          # 存在性
assert poco("Check").attr("visible")   # 可见性

5.3 完整业务流程脚本

python

复制代码
# game_flow_test.py
"""
完整游戏业务流程自动化
登录 → 主界面 → 任务 → 战斗 → 结算
"""
from airtest.core.api import *
from poco.drivers.unity3d import UnityPoco
import allure

poco = UnityPoco()


class GameAutoTest:
    """游戏自动化测试类"""
    
    def setup_method(self):
        auto_setup(__file__)
    
    @allure.step("登录游戏")
    def login(self, account, password):
        poco(text="登录").wait_for_appearance(timeout=30)
        poco("AccountInput").set_text(account)
        poco("PasswordInput").set_text(password)
        poco("LoginBtn").click()
        poco("MainPanel").wait_for_appearance(timeout=30)
    
    @allure.step("领取日常任务")
    def claim_daily_task(self):
        poco("TaskBtn").click()
        poco("DailyTab").click()
        # 一键领取
        if poco("ClaimAllBtn").exists():
            poco("ClaimAllBtn").click()
            poco("ConfirmBtn").click()
        poco("CloseBtn").click()
    
    @allure.step("进入副本战斗")
    def enter_battle(self, stage_name):
        poco("BattleBtn").click()
        poco(text=stage_name).click()
        poco("StartBtn").click()
        # 等待战斗界面
        poco("BattleUI").wait_for_appearance(timeout=30)
    
    @allure.step("自动战斗")
    def auto_battle(self):
        # 开启自动战斗
        poco("AutoBtn").click()
        # 等待战斗结束
        poco("BattleResult").wait_for_appearance(timeout=180)
    
    @allure.step("验证战斗结果")
    def verify_result(self):
        assert poco("BattleResult").exists()
        result = poco("ResultText").get_text()
        assert "胜利" in result or "成功" in result, f"战斗失败: {result}"
    
    def test_full_flow(self):
        """完整流程测试"""
        self.login("test_account", "test_password")
        self.claim_daily_task()
        self.enter_battle("第一章第一关")
        self.auto_battle()
        self.verify_result()


if __name__ == '__main__':
    test = GameAutoTest()
    test.setup_method()
    test.test_full_flow()

六、搭建游戏自动化框架

6.1 框架目录结构

6.2 游戏基类封装

python

复制代码
# base/base_game.py
"""游戏自动化基类"""
from airtest.core.api import *
from poco.drivers.unity3d import UnityPoco
import logging

logger = logging.getLogger(__name__)


class BaseGame:
    """游戏基类"""
    
    def __init__(self):
        self.poco = UnityPoco()
    
    def click(self, locator):
        """点击控件(支持控件名和文本)"""
        if locator.startswith("text="):
            self.poco(text=locator[5:]).click()
        else:
            self.poco(locator).click()
        logger.info(f"点击: {locator}")
    
    def wait_element(self, locator, timeout=15):
        """等待元素出现"""
        if locator.startswith("text="):
            self.poco(text=locator[5:]).wait_for_appearance(timeout)
        else:
            self.poco(locator).wait_for_appearance(timeout)
    
    def is_exist(self, locator, timeout=3):
        """判断元素是否存在"""
        try:
            self.wait_element(locator, timeout)
            return True
        except Exception:
            return False
    
    def get_text(self, locator):
        """获取控件文本"""
        return self.poco(locator).get_text()
    
    def input_text(self, locator, text):
        """输入文本"""
        self.poco(locator).set_text(text)
    
    def screenshot(self, name):
        """截图(用于报告)"""
        from airtest.core.api import snapshot
        import os
        os.makedirs("reports/screenshots", exist_ok=True)
        path = f"reports/screenshots/{name}.png"
        snapshot(filename=path)
        return path

6.3 配置化设备管理

yaml

复制代码
# config/config.yaml
devices:
  - serial: "emulator-5554"
    game_package: "com.company.game"
    resolution: [1280, 720]
  - serial: "emulator-5556"
    game_package: "com.company.game"
    resolution: [1920, 1080]

accounts:
  test_user:
    account: "test_account"
    password: "test_password"
  vip_user:
    account: "vip_account"
    password: "vip_password"

timeouts:
  login: 30
  battle: 180
  default: 15

python

复制代码
# utils/driver_util.py
"""多设备管理"""
import yaml
from airtest.core.api import connect_device, set_current


def load_config(path="config/config.yaml"):
    with open(path, encoding='utf-8') as f:
        return yaml.safe_load(f)


def connect_all_devices():
    """连接配置中的所有设备"""
    config = load_config()
    devices = []
    for dev in config['devices']:
        serial = dev['serial']
        conn = connect_device(f"Android:///{serial}")
        devices.append({
            'serial': serial,
            'conn': conn,
            'config': dev
        })
    return devices

七、多设备并行执行

7.1 并行执行方案

python

复制代码
# parallel_runner.py
"""多设备并行执行游戏自动化"""
import threading
from airtest.core.api import connect_device, set_current
from utils.driver_util import load_config


def run_case_on_device(serial, case_func):
    """在指定设备上运行用例"""
    conn = connect_device(f"Android:///{serial}")
    set_current(serial)  # 切换当前设备
    try:
        case_func()
        print(f"[{serial}] 测试通过")
    except Exception as e:
        print(f"[{serial}] 测试失败: {e}")


def run_parallel(case_func):
    """在所有设备上并行运行"""
    config = load_config()
    threads = []
    for dev in config['devices']:
        t = threading.Thread(
            target=run_case_on_device,
            args=(dev['serial'], case_func)
        )
        threads.append(t)
        t.start()
    
    for t in threads:
        t.join()
    print("所有设备执行完成")


if __name__ == '__main__':
    from cases.test_login import test_login
    run_parallel(test_login)

7.2 并行执行的注意事项

复制代码
注意事项:
  1. 账号隔离:每台设备用不同账号,避免互相干扰
  2. 数据隔离:确保测试数据不冲突
  3. 资源隔离:避免设备间网络/资源竞争
  4. 结果汇总:统一收集各设备结果
  5. 异常处理:单设备失败不影响其他设备
  6. 截图标注:截图按设备区分,方便定位

八、自动化接入CI/CD

8.1 Jenkins Pipeline配置

groovy

复制代码
// Jenkinsfile
pipeline {
    agent any
    
    parameters {
        choice(name: 'DEVICE', choices: ['emulator-5554', 'emulator-5556'], description: '测试设备')
        string(name: 'BRANCH', defaultValue: 'master', description: '测试包分支')
    }
    
    stages {
        stage('准备环境') {
            steps {
                sh 'pip install -r requirements.txt'
                // 连接设备
                sh 'adb connect ${DEVICE}'
            }
        }
        
        stage('安装游戏包') {
            steps {
                sh 'adb -s ${DEVICE} install -r build/game.apk'
            }
        }
        
        stage('执行自动化') {
            steps {
                sh '''
                    pytest cases/ \
                        --alluredir=reports/allure-results \
                        -v
                '''
            }
            post {
                always {
                    allure includeProperties: false,
                        results: [[path: 'reports/allure-results']]
                }
            }
        }
        
        stage('清理环境') {
            steps {
                sh 'adb -s ${DEVICE} uninstall com.company.game'
            }
        }
    }
}

8.2 触发时机建议

复制代码
自动化触发时机:
  ├── 每日凌晨:全量回归
  ├── 提测时:核心流程冒烟
  ├── 版本构建后:安装验证
  ├── 上线前:关键功能回归
  └── 定时巡检:稳定性监控

九、游戏自动化的边界

9.1 适合自动化的场景

复制代码
✅ 适合自动化:
  ├── 稳定的UI流程(登录、进入游戏)
  ├── 重复性高的回归测试
  ├── 多设备兼容性冒烟
  ├── 长时间稳定性测试(挂机)
  ├── 性能数据采集
  └── 数值批量验证

9.2 不适合自动化的场景

复制代码
❌ 不适合自动化:
  ├── 主观体验测试(手感、画面)
  ├── 探索式测试
  ├── 一次性的功能测试
  ├── 频繁变动的UI
  ├── 需要人工判断的场景
  └── 复杂战斗操作(需要人类反应)

9.3 自动化的正确认知

复制代码
自动化不是万能的:
  ├── 自动化是"辅助",不是"替代"
  ├── 自动化适合"重复",不适合"探索"
  ├── 自动化投入产出比要算清楚
  └── 维护成本可能比开发成本更高

正确姿势:
  用自动化解决重复劳动
  把人力投入到更有价值的探索测试

十、总结

10.1 核心要点

要点 一句话
游戏自动化难点 UI自绘、画面动态、引擎多样
技术选型 图像识别(Airtest) + 控件识别(Poco)
控件识别优先 有控件树就用Poco,更准更快
图像识别兜底 无控件树时用图像匹配
框架搭建 基类封装 + 页面对象 + 配置化
并行执行 多设备同时跑,提升效率
合理定位 自动化做回归,人工做探索

10.2 游戏自动化学习路径

复制代码
入门:会用AirtestIDE录制脚本
  ↓
进阶:会写Poco控件识别脚本
  ↓
熟练:能搭建自动化框架
  ↓
高级:多设备并行 + CI集成
  ↓
专家:能结合性能/数值做综合自动化

10.3 下一篇预告

📌 下一篇:《游戏测试专题第八篇:游戏安全测试与反外挂》------内存修改、协议篡改、自动化脚本,游戏安全测试的攻防实战。
🔥 外挂是游戏的天敌,安全测试是游戏测试里技术门槛最高的方向。下篇见。

相关推荐
2302_805771072 小时前
Day13Jmeter数据驱动
功能测试·单元测试·测试用例·需求分析·测试
ClouGence7 小时前
Chrome Recorder 能用于长期回归测试吗?
前端·chrome·测试
一孤程7 小时前
游戏测试专题第八篇:游戏安全测试与反外挂
游戏·测试
ClouGence10 小时前
开发提效:CueCast MCP 构建自动化测试 Agent 工作流
agent·测试·mcp
2302_8057710715 小时前
Day18在接口自动化测试中引入pytest用例管理框架
功能测试·单元测试·测试用例·需求分析·测试
河南花仙子科技1 天前
小游戏社交裂变功能的高阶设计思路
大数据·科技·游戏
一孤程2 天前
游戏测试专题第五篇:游戏兼容性测试与多端适配
游戏·测试
狂人开飞机2 天前
23、实战平台跳跃游戏
游戏·游戏引擎·godot