游戏自动化测试

用自动化工具保证游戏质量,从"只会手动点按钮测试"到"能覆盖核心玩法的全链路测试"

前置知识:第04章 代码质量与工程实践(单元测试基础)


阅读指南(初学者必看)

为什么你需要学习游戏自动化测试?

手动测试的问题:

  • 每次改代码都要手动回归测试 → 浪费时间
  • 人工测试容易遗漏边界情况 → 线上出 Bug
  • 压力测试不可能手动模拟 1 万个玩家
  • 弱网测试手动模拟不了

学完本章,你能回答:

  • Airtest 怎么做 UI 自动化测试?图像识别和控件定位怎么选?
  • Locust 怎么做压力测试?怎么模拟万级并发?
  • 弱网测试怎么做?有哪些工具?
  • 什么是测试左移?怎么在 CI/CD 中设置质量门禁?
  • 游戏测试和 Web 测试有什么区别?

本文结构

第一部分:游戏测试体系总览
第二部分:UI 自动化测试(Airtest)
第三部分:压力测试(Locust / JMeter / k6)
第四部分:弱网测试
第五部分:测试左移与质量门禁
第六部分:兼容性测试

一、游戏测试体系总览

生活类比:游戏测试就像"汽车质量检测"——有零件级检测(单元测试)、组装检测(集成测试)、上路检测(E2E测试)、极限检测(压力测试)。

测试金字塔

           /\
          /  \        E2E测试(少量,慢,贵)
         /    \       - Airtest 自动化
        /------\      
       /        \     集成测试(适量)
      /          \    - API 测试、房间流程测试
     /------------\   
    /              \   单元测试(大量,快,便宜)
   /                \  - 伤害计算、背包逻辑、匹配算法
  /------------------\

游戏测试 vs Web 测试

Web 测试 游戏测试
UI 测试 Selenium/Playwright Airtest/Poco
API 测试 HTTP 接口 WebSocket + Protobuf
性能测试 HTTP 压测 WebSocket 长连接压测
特殊测试 弱网、帧率、内存泄漏
难点 实时性、随机性、时序

二、UI 自动化测试(Airtest)

生活类比:Airtest 就像"机器人帮你点手机"——你告诉它点哪里、输入什么,它自动操作并检查结果。

Airtest 基础

from airtest.core.api import *
from poco.drivers.unity import UnityPoco

# 连接设备
connect_device("Android:///")
poco = UnityPoco()

def test_login():
    """测试登录流程"""
    # 点击登录按钮
    poco("btnLogin").click()
    
    # 输入账号密码
    poco("inputAccount").set_text("testuser")
    poco("inputPassword").set_text("testpass")
    
    # 确认登录
    poco("btnConfirm").click()
    
    # 验证登录成功
    assert poco("lblUsername").get_text() == "testuser"

def test_battle():
    """测试战斗流程"""
    poco("btnBattle").click()
    poco("battleScene").wait_for_appearance(timeout=10)
    
    poco("btnSkill1").click()
    sleep(1)
    poco("btnSkill2").click()
    
    poco("battleResult").wait_for_appearance(timeout=60)
    assert poco("lblResult").get_text() == "胜利"

图像识别测试

# 不依赖 UI 控件名,用图像识别
from airtest.core.api import *

def test_login_by_image():
    # 截图模板匹配
    touch(Template("login_button.png"))
    wait(Template("main_scene.png"), timeout=10)
    
    # 断言某个元素存在
    exists(Template("vip_icon.png"))

Poco 控件定位(推荐)

为什么需要 Poco?图像识别有个问题:分辨率变了、UI 皮肤换了,截图就失效了。Poco 像视力好外加懂结构的测试员,通过控件树定位元素。

接入方式(以 Unity 游戏为例):

  1. 在 Unity 中导入 Poco SDK
  2. 游戏中开启 Poco 服务
  3. 测试脚本通过 Poco 获取控件树
from airtest.core.api import *
from poco.drivers.unity3d import UnityPoco

auto_setup(__file__)
poco = UnityPoco()

# 通过控件名点击
poco("btn_start").click()

# 通过路径定位
poco("Canvas").child("MainMenu").child("btn_start").click()

# 通过文本内容定位
poco(text="开始游戏").click()

# 获取控件属性
name = poco("player_name").get_text()
assert name == "expected_name", f"名字不匹配: {name}"

# 判断控件是否存在
if poco("popup_reward").exists():
    poco("btn_claim").click()

# 遍历列表控件
for item in poco("scroll_view").child("item"):
    item.click()
    sleep(0.5)

完整实战:登录到完成新手引导

from airtest.core.api import *
from poco.drivers.unity3d import UnityPoco
import unittest

class TestNewbieGuide(unittest.TestCase):
    
    @classmethod
    def setUpClass(cls):
        auto_setup(__file__)
        cls.poco = UnityPoco()
        cls.device = device()
    
    def test_login_and_guide(self):
        # 1. 等待登录界面
        self.poco("btn_login").wait_for_appearance(timeout=10)
        
        # 2. 输入账号密码
        self.poco("input_account").set_text("test001")
        self.poco("input_password").set_text("test123")
        
        # 3. 点击登录
        self.poco("btn_login").click()
        
        # 4. 等待进入主界面
        self.poco("main_ui").wait_for_appearance(timeout=30)
        
        # 5. 检查是否弹出新手引导
        if self.poco("newbie_guide_mask").exists():
            # 按引导点击
            for i in range(5):
                guide_btn = self.poco("guide_highlight")
                if guide_btn.exists():
                    guide_btn.click()
                    sleep(1)
                else:
                    break
        
        # 6. 断言新手引导完成
        self.assertTrue(
            self.poco("guide_complete").exists(),
            "新手引导未完成"
        )
    
    @classmethod
    def tearDownClass(cls):
        # 清理:回到初始状态
        cls.poco("btn_settings").click()
        cls.poco("btn_logout").click()

if __name__ == "__main__":
    unittest.main()

Airtest IDE 与报告

Airtest IDE 是可视化编辑工具:

  1. 连接设备(真机或模拟器)
  2. 左侧截图辅助区截图
  3. 右侧脚本编辑器编写逻辑
  4. 点击运行看效果

批量运行测试:

# 单脚本运行
airtest run test_login.air --device Android:/// --log log_dir

# 生成 HTML 报告
airtest report test_login.air --log_root log_dir --outfile report.html

三、压力测试(Locust / JMeter / k6)

生活类比:压力测试就像"模拟万人抢购"——用脚本模拟大量用户同时操作,看服务器能不能扛住。

Locust 基础(Python 代码化)

from locust import HttpUser, task, between
import random

class GamePlayer(HttpUser):
    wait_time = between(1, 3)
    
    def on_start(self):
        """登录"""
        self.client.post("/api/login", json={
            "account": f"player_{self.id}",
            "password": "test123"
        })
    
    @task(3)
    def get_player_info(self):
        """获取玩家信息"""
        self.client.get("/api/player/info")
    
    @task(2)
    def get_ranking(self):
        """查询排行榜"""
        self.client.get("/api/ranking")
    
    @task(1)
    def join_room(self):
        """加入房间"""
        self.client.post("/api/room/join", json={
            "mode": "ranked"
        })

# 运行:locust -f game_load_test.py --host=http://game-server:8080
# Web UI:http://localhost:8089

WebSocket 压力测试

from locust import User, task, events
import websocket
import json

class GameWebSocketUser(User):
    def on_start(self):
        self.ws = websocket.create_connection("ws://game-server:8080/ws")
    
    @task
    def send_move(self):
        """模拟移动消息"""
        msg = {
            "type": "move",
            "x": 100.0,
            "y": 200.0,
            "frame": 12345
        }
        self.ws.send(json.dumps(msg))
        response = self.ws.recv()
    
    def on_stop(self):
        self.ws.close()

JMeter 压测(GUI 工具)

适用场景:HTTP 接口压测,适合非程序员使用

基础配置步骤:

  1. 下载 JMeter,启动 GUI
  2. 右键 Test Plan -> Add -> Threads -> Thread Group
  3. 配置线程数(模拟用户数)、Ramp-Up 时间、循环次数
  4. 添加 HTTP Request Sampler
  5. 添加 Listener(View Results Tree / Summary Report)

k6 压测(JavaScript,适合开发者)

import http from 'k6/http';
import { check, sleep } from 'k6';

// 压测配置:模拟 1000 并发,持续 5 分钟
export const options = {
  stages: [
    { duration: '2m', target: 100 },   // 2分钟 ramp up 到 100
    { duration: '5m', target: 1000 },  // 5分钟 ramp up 到 1000
    { duration: '2m', target: 1000 },  // 保持 1000 并发 2 分钟
    { duration: '2m', target: 0 },     // 2分钟 ramp down
  ],
  thresholds: {
    http_req_duration: ['p(95)<500'],  // 95% 请求小于 500ms
    http_req_failed: ['rate<0.01'],    // 错误率小于 1%
  },
};

export default function () {
  const loginRes = http.post('http://localhost:8080/api/login', JSON.stringify({
    username: 'user_test',
    password: 'test123',
  }), {
    headers: { 'Content-Type': 'application/json' },
  });
  
  check(loginRes, {
    'login success': (r) => r.status === 200,
    'has token': (r) => r.json('token') !== undefined,
  });
  
  sleep(1);
}

运行命令:k6 run script.js

压测指标与目标

指标 目标值 说明
QPS > 10,000 每秒处理请求数
P50 延迟 < 50ms 50% 请求的响应时间
P99 延迟 < 200ms 99% 请求的响应时间
错误率 < 0.1% 请求失败比例
CPU 使用率 < 70% 服务器 CPU
内存使用率 < 80% 服务器内存

压测分析要点

压测结果分析清单:

1. 吞吐量是否达标?
   - 目标 TPS / 实际 TPS
   
2. 响应时间是否可接受?
   - P50 / P90 / P99 分别多少
   - 是否有明显长尾延迟
   
3. 错误率在合理范围?
   - 4xx 错误(客户端问题)
   - 5xx 错误(服务端问题)
   - 超时错误
   
4. 资源使用是否正常?
   - CPU 使用率
   - 内存使用趋势(是否有泄漏)
   - 数据库连接数
   - 网络带宽
   
5. 瓶颈在哪?
   - 数据库查询慢?
   - 锁竞争?
   - 第三方接口延迟?
   - 内存不足导致 GC 频繁?

四、弱网测试

生活类比:弱网测试就像"在信号差的地方打电话"——模拟网络不好的场景,看游戏是否还能正常工作。

弱网模拟工具

工具 平台 功能
Clumsy Windows 延迟、丢包、乱序、重复
Charles 跨平台 HTTP 限速、丢包
Network Link Conditioner macOS 系统级弱网模拟
Linux TC Linux 最灵活,适合服务端

Charles 弱网模拟

适用:HTTP/HTTPS 接口测试

步骤:

  1. 安装 Charles,设置系统代理
  2. Proxy -> Throttle Settings
  3. 勾选 Enable Throttling
  4. 选择或自定义网络配置:
场景 带宽 延迟 丢包
4G 20 Mbps 50ms 0%
3G 1 Mbps 200ms 0%
地铁 500 Kbps 500ms 5%
电梯 50 Kbps 2000ms 20%

Clumsy 弱网模拟(Windows)

Clumsy 是 Windows 上的网络损伤工具,可以模拟:

  • Lag(延迟)
  • Drop(丢包)
  • Throttle(带宽限制)
  • Out of order(乱序)
  • Tamper(篡改)

使用方法:

  1. 下载 Clumsy,以管理员身份运行
  2. Filtering 输入条件:udp.DstPort == 7777 or tcp.DstPort == 7777
  3. 勾选 Lag,设置 200ms
  4. 勾选 Drop,设置 5%
  5. 点击 Start

Linux TC 弱网模拟

# 添加 100ms 延迟 + 5% 丢包
tc qdisc add dev eth0 root netem delay 100ms loss 5%

# 模拟 3G 网络(延迟 200ms,带宽 1.5Mbps)
tc qdisc add dev eth0 root netem delay 200ms rate 1.5mbit

# 清除
tc qdisc del dev eth0 root

必测弱网场景

场景 测试重点
登录时断网再恢复 能否自动重连
战斗中丢包 10% 状态是否同步
延迟 500ms 操作手感是否可接受
断线 30 秒后重连 能否恢复战斗
弱网下聊天消息 消息是否丢失/乱序
弱网下支付 是否重复扣款

五、测试左移与质量门禁

什么是测试左移?

传统模式:开发 → 测试 → 上线 测试左移:在开发阶段就引入质量保障,问题发现越早,修复成本越低。

Bug 修复成本:
开发阶段发现 → 5 分钟修复
Code Review 发现 → 30 分钟修复
测试阶段发现 → 2 小时修复
线上发现 → 2 天修复 + 玩家流失 + 品牌损失

质量门禁(Quality Gate)

在 CI/CD 流水线中设置检查点,不通过就阻止代码合并:

门禁阶段 检查内容 工具
提交前 代码格式、Lint ESLint, Prettier
构建时 编译是否通过 编译器
测试时 单元测试通过率 Jest, JUnit
扫描时 代码质量、安全漏洞 SonarQube, Trivy
合并前 Code Review 通过 PR 强制 Review

SonarQube 代码质量扫描

# GitHub Actions 中集成 SonarQube
- name: SonarQube Scan
  uses: sonarqube-quality-gate-action@master
  with:
    scanMetadataReportFile: .scannerwork/report-task.txt
  timeout-minutes: 5

质量门禁规则示例:

  • 代码覆盖率 >= 60%
  • 严重 Bug 数 = 0
  • 安全漏洞 = 0
  • 代码重复率 < 5%
  • 技术债比率 < 5%

六、兼容性测试

游戏兼容性测试维度

维度 具体内容 测试重点
设备 不同品牌手机、平板 华为、小米、三星、iPhone 等
系统版本 Android 8-14, iOS 12-17 低版本系统兼容性
分辨率 720p, 1080p, 2K, 异形屏 UI 适配、截断问题
硬件性能 高端机、中端机、低端机 帧率、发热、耗电
渲染 API OpenGL ES, Vulkan, Metal 图形渲染差异
网络 WiFi, 4G, 5G, 弱网 网络切换处理

兼容性测试矩阵

机型 华为 小米 OPPO vivo 三星 iPhone 14 iPhone 11 iPad
高端 P40 14 Find X90 S23 V V V
中端 nova 12 Reno S16 A54 V - -
低端 畅享 Redmi A系列 Y系列 A系列 - - -

V = 必须测试

  • = 可选测试

低端机性能基线

  • 冷启动时间小于 15 秒
  • 登录界面 FPS 大于 25
  • 主城场景 FPS 大于 20
  • 战斗场景 FPS 大于 20
  • 内存峰值小于 1.5GB
  • 连续游玩 30 分钟不闪退
  • 机身温度小于 45 度

自问自答

Q1:自动化测试能替代手动测试吗?

不能。自动化擅长回归测试和压力测试,但游戏体验、手感、视觉效果只能人测。自动化 + 手动 = 完整的测试体系。建议比例:自动化 70% + 人工 30%。

Q2:压力测试应该模拟多少用户?

目标在线的 23 倍。如目标 1 万在线,压测 23 万。考虑峰值(活动、开服)可能达到平均的 5~10 倍。

Q3:Airtest 支持 H5 游戏吗?

支持。Airtest 可以测试 Web 页面和微信小游戏。但 H5 游戏的 DOM 元素定位比 Unity 原生控件麻烦。

Q4:游戏逻辑怎么写单元测试?

核心原则:把逻辑和引擎分离。伤害计算、背包操作、匹配算法这些纯逻辑写成独立函数,用 JUnit/Mocha 做单元测试。UI 和渲染不做单元测试。

Q5:压测时数据库怎么办?

用独立的压测数据库,不要用生产数据。压测完清空。或者用 Testcontainers 启动临时数据库。压测前准备与生产环境相当的数据量。

Q6:Airtest 和 Appium 有什么区别? A: Airtest 更适合游戏(基于图像识别,不依赖控件树);Appium 更适合 App(基于原生控件定位)。游戏因为用 Unity/Unreal 渲染,控件树不透明,所以 Airtest 更实用。

Q7:兼容性测试设备太多,买不起怎么办? A: 使用云测平台(WeTest、Testin、Firebase Test Lab),按分钟或按次付费。优先覆盖 TOP 20 机型,再逐步扩展。


实践任务

  1. 编写 Airtest 脚本:测试登录 → 创建房间 → 战斗 → 结算完整流程
  2. 编写 Locust 压测:模拟 1 万玩家在线,目标 P99 < 200ms
  3. 搭建弱网环境:用 Clumsy 模拟延迟 200ms + 丢包 5%,测试断线重连
  4. 编写单元测试:为伤害计算、背包操作写单元测试,覆盖率 > 80%
  5. 配置质量门禁:在 CI/CD 中集成 SonarQube,覆盖率低于 60% 阻止合并
  6. 制定测试计划:为新版本制定完整的测试计划(自动化+手动+压测+弱网)

初学者常见错误

错误1:Airtest 截图分辨率不匹配

问题: 在 1080p 设备上截图,在 720p 设备上运行识别失败。 解决: 使用 resolution 参数,或在多套分辨率上分别截图。

错误2:Poco 控件在动态列表中定位失败

问题: 列表滚动后,控件索引变化。 解决: 用文本内容或属性定位,不要用索引;先滚动到可见区域再操作。

错误3:压测脚本没有模拟真实用户行为

问题: 所有虚拟用户同时执行相同操作,形成脉冲式请求。 解决: 使用 wait_time 添加随机间隔;不同用户执行不同操作序列。

错误4:压测时忽略了数据库初始数据

问题: 空数据库压测很快,有 1000 万条数据后慢如蜗牛。 解决: 压测前准备与生产环境相当的数据量。

错误5:弱网测试只测了延迟,没测丢包

问题: 游戏对丢包更敏感,TCP 重传会导致卡顿。 解决: 组合测试:高延迟 + 丢包 + 带宽限制。


与其他章节的关联

本节内容 关联章节 关联点
单元测试 第04章 代码质量与工程实践 单元测试是自动化的基础
CI/CD 第01章 CI/CD 流水线 自动化测试集成到流水线
性能监控 第06章 游戏性能监控 压测时需要监控系统指标
WebSocket 3_1 第14章 游戏实时通信优化 WebSocket 压测的特殊处理
弱网优化 3_1 第14章 游戏实时通信优化 弱网测试验证优化效果
质量门禁 第04章 代码质量与工程实践 测试左移与 Code Review 配合

⬅️ 上一章:代码质量与工程实践 | ➡️ 下一章:游戏性能监控