游戏自动化测试
用自动化工具保证游戏质量,从"只会手动点按钮测试"到"能覆盖核心玩法的全链路测试"
前置知识:第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 游戏为例):
- 在 Unity 中导入 Poco SDK
- 游戏中开启 Poco 服务
- 测试脚本通过 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 是可视化编辑工具:
- 连接设备(真机或模拟器)
- 左侧截图辅助区截图
- 右侧脚本编辑器编写逻辑
- 点击运行看效果
批量运行测试:
# 单脚本运行
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 接口压测,适合非程序员使用
基础配置步骤:
- 下载 JMeter,启动 GUI
- 右键 Test Plan -> Add -> Threads -> Thread Group
- 配置线程数(模拟用户数)、Ramp-Up 时间、循环次数
- 添加 HTTP Request Sampler
- 添加 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 接口测试
步骤:
- 安装 Charles,设置系统代理
- Proxy -> Throttle Settings
- 勾选 Enable Throttling
- 选择或自定义网络配置:
| 场景 | 带宽 | 延迟 | 丢包 |
|---|---|---|---|
| 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(篡改)
使用方法:
- 下载 Clumsy,以管理员身份运行
- Filtering 输入条件:
udp.DstPort == 7777 or tcp.DstPort == 7777 - 勾选 Lag,设置 200ms
- 勾选 Drop,设置 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:压力测试应该模拟多少用户?
目标在线的 2
3 倍。如目标 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 机型,再逐步扩展。
实践任务
- 编写 Airtest 脚本:测试登录 → 创建房间 → 战斗 → 结算完整流程
- 编写 Locust 压测:模拟 1 万玩家在线,目标 P99 < 200ms
- 搭建弱网环境:用 Clumsy 模拟延迟 200ms + 丢包 5%,测试断线重连
- 编写单元测试:为伤害计算、背包操作写单元测试,覆盖率 > 80%
- 配置质量门禁:在 CI/CD 中集成 SonarQube,覆盖率低于 60% 阻止合并
- 制定测试计划:为新版本制定完整的测试计划(自动化+手动+压测+弱网)
初学者常见错误
错误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 配合 |
⬅️ 上一章:代码质量与工程实践 | ➡️ 下一章:游戏性能监控