Python循环在游戏测试自动化中的实战应用与避坑指南

发布时间:2026/9/19 4:10:09
Python循环在游戏测试自动化中的实战应用与避坑指南 不做理论铺垫直接说一个每天真实发生的场景你拿到一个新版本要测十来个角色的技能释放是否正常每个角色又有普攻、小技能、大招三档动作。手点的话一个角色至少三分钟十个角色不停歇也得半小时。而且这种活本周还得再来三遍因为版本又提测了。这种东西代码层面就是一个循环能解决的问题Python 写出来还特别短。这篇就来梳理一下游戏测试场景下循环语句到底怎么用怎么把重复劳动塞给脚本以及新手最容易踩的坑。如果你刚入门 Python或者会用一点但没怎么在测试里写过正经脚本这篇内容就是冲着你来的。循环是整个自动化测试的地基游戏测试又刚好是循环语句的黄金应用场——批量、重复、反复验证、跑数值、压操作。把这块吃透后面接 pytest、unittest、UI 自动化这些框架你会感觉顺滑很多。1. 游戏测试为什么会跟循环“锁死”1.1 游戏测试里的三种重复劳动先盘一下游戏测试这个岗位上日常到底在重复什么。第一种是“同一套动作跑多遍”比如登录、选角、进副本、放技能、退出结算每天少说跑几十回。第二种是“同一份数据测多个角色”角色多、技能多、装备多每个都要跑一遍验证。第三种是“盯着某个值反复横跳”测掉率、测伤害波动、测 buff 持续时间需要打好几百个点去确认数值在合理范围。这三种劳动有一个共性操作规律、结果可判断、流程可重复。这恰好就是循环语句擅长解决的事情。用人的手去点第一遍你可能认真第十遍就犯困了第二十遍眼睛都花了。用脚本循环去跑每一遍都一视同仁不会因为熬夜加班就漏掉某个判定。我经常跟团队里的新人说一句话在游戏测试里最不值钱的就是“重复点击”最值钱的也是“把重复点击变成脚本的能力”。前者消耗的是你的耐心后者积累的是你的脚本库。1.2 为什么偏偏是 Python可能有同学会问做自动化不是有很多语言吗Java、C#、Lua 不也能写确实能写但游戏测试这个场景比较特殊。游戏的 CI 流程、测试工具链、外部工具脚本Python 的出场率是最高的。它上手快没有太多语法门槛写个几十行的验证脚本不需要引入一堆工程化配置。Python 在循环这个语法点上也非常直白。for 就是“遍历”while 就是“当条件成立就继续”跟自然语言的逻辑几乎一一对应。你不需要了解编译原理不需要搞懂迭代器接口怎么实现只要会用 range、会遍历列表就已经能写出一大堆能跑的东西了。这个“低门槛”很关键因为测试人员的本职是找 bug不是花大量精力在编程语言本身你只需要用最小成本把工具搭出来。还有一个现实原因游戏公司里的测试团队普遍规模不大没有专职的测试开发给你兜底。你写脚本的时候能依赖的技术栈越简单越好。Python 这种“脚本式”语言改起来快、跑起来直接非常适合测试这种迭代频繁的活。2. 先聊 for 循环游戏测试里的一等公民2.1 遍历那些要“批量处理”的测试数据游戏测试里最常见的 for 循环场景就是拿到一批测试数据然后逐个处理。比如说测试配置表里列出了所有角色 ID你要验证每个角色的初始金币是不是 5000。这串代码用 for 写起来会非常顺role_ids [1001, 1002, 1003, 1004, 1005] # 期望每个角色初始金币都是 5000 for role_id in role_ids: gold get_role_gold(role_id) # 模拟从游戏接口拿金币 if gold ! 5000: print(f角色 {role_id} 金币异常期望 5000实际 {gold}) else: print(f角色 {role_id} 金币正常)思路就是把数据放进一个可迭代的容器里让 for 自己去“拿出来处理再拿下一个”。角色数量再多也不慌因为代码只写了一行循环逻辑剩下的是数据量的差异。之前有个测试同学跟我吐槽他们组测一个带 120 个英雄的手游光角色初始配置就验了大半天。后来我把这串脚本给他跑一轮只要十几秒。上面这段代码里get_role_gold 是一个占位函数实际项目里可能是调游戏的后端接口也可能是读数据库或者解析日志。这是测试脚本里很典型的写法——核心的循环结构负责“反复执行”具体业务逻辑封装在函数里方便替换。这样脚本的复用性也高今天验金币明天验战力改一行调用就行。2.2 range、enumerate、zip遍历时的三个高频伙伴很多初学循环的同学只会写 for x in list遇到“我要循环十次”“我要同时拿索引和值”“我要同时遍历两个列表”就卡住了。这三个场景游戏测试里可谓天天碰到对应的解法就是 range、enumerate、zip。range 的经典用途是“按次数循环”。做连点测试的时候你要模拟玩家连续点击某个按钮一百次for i in range(100): click_button(attack) time.sleep(0.1) # 每次点击间隔 0.1 秒这里不关心 i 具体是什么只关心循环执行了多少遍。真正做连点测试的时候我们还会记录第几次点击后出现了异常这时候 i 就有价值了当 i 等于 87 的时候页面崩了那这个 87 就是高价值的复现线索。所以 range 不只是“循环十次”它给你的那个计数变量本身就是测试数据的一部分。enumerate 解决的是“既要元素又要序号”的问题。测试关卡配置表的时候你想把每个关卡和它在列表里的位置一起打印出来levels [training_01, forest_02, cave_03, boss_04] for idx, level in enumerate(levels): print(f第 {idx 1} 个关卡{level})zip 则是在验证“一一对应关系”的时候特别有用。比如你要验证每个角色的模型资源 ID 和战斗技能 ID 是否对得上两个列表的位置一一对应role_ids [1001, 1002, 1003] model_ids [m_1001, m_1002, m_1003] skill_ids [s_1001, s_1002, s_1003] for role_id, model_id, skill_id in zip(role_ids, model_ids, skill_ids): if model_id ! fm_{role_id} or skill_id ! fs_{role_id}: print(f角色 {role_id} 资源ID不匹配)zip 的方便之处是它自动按最短的列表收束。如果有一条数据的某个字段缺失了zip 会默默忽略掉超出的部分这在排查“配置长度不一致”的时候反而是个快速定位的手段。2.3 来个“全角色技能检查”小脚本上面讲了一些零碎用法把它们组合起来做一个真实的小脚本会更有体感。假设我们有一个角色配置文件里面存了每个角色的技能 ID 列表我们要验证每个角色至少有一个普攻技能和一个大招技能role_config { 1001: {skills: [9001, 9002, 9003], role_type: warrior}, 1002: {skills: [9101], role_type: mage}, 1003: {skills: [9201, 9202, 9203, 9204], role_type: assassin}, } for role_id, config in role_config.items(): skill_list config[skills] has_normal 9001 in skill_list # 假设 9001 是普攻 has_ultimate any(skill 9300 for skill in skill_list) # 假设大招在 9300 if not has_normal: print(f角色 {role_id} 缺少普攻技能) if not has_ultimate: print(f角色 {role_id} 缺少大招技能) if has_normal and has_ultimate: print(f角色 {role_id} 技能配置正常)这个脚本的逻辑很简单但它很能说明 for 循环在工作里的价值一个角色和一百个角色代码量是一样的。你只负责把判定规则写出来剩下的交给循环。新手写这种脚本最容易犯的错是忘记缩进for 下面的 print 忘缩进就会导致只打印最后一个角色。这种低级错误跑一次就能发现因为输出明显不对。3. 再聊 while 循环让脚本会“看情况办事”3.1 什么时候非用 while 不可for 循环适合“我知道要跑几遍”的场景但游戏测试里有一类场景是“我不知道多久能结束但我知道结束条件”。举个例子测游戏启动流程你点击开始按钮之后游戏会进入加载界面你没法预估加载要花多少秒只能一直轮询检查“加载是否完成”。这种场景用 while 循环更自然load_finished False timeout 60 # 最长等待 60 秒 start_time time.time() while not load_finished and time.time() - start_time timeout: load_finished check_loading_status() # 轮询加载状态 time.sleep(0.5) if load_finished: print(加载完成进入游戏主界面) else: print(加载超时疑似死循环或卡资源)这段代码里有几个精髓。第一while 的条件是“不满足就继续”所以我们写的是 not load_finished。第二加了一个 timeout 上限防止游戏真的卡死的时候脚本也跟着永远跑下去。第三sleep(0.5) 是给 CPU 喘气的机会测试脚本没必要一秒轮询几百次反而会给服务端造成没必要的压力。游戏测试里还有一类经典场景自动化跑图走到悬崖边角色会被挡住走到墙角镜头会抖动。这种位置变化不是固定次数的只能靠 while 循环前进直到撞到不可通过的碰撞体或者到达目标点。用 while 来处理这种“动态条件”特别合适。3.2 死循环恶魔跟天使只隔一层膜很多新手谈“死循环”色变总觉得这是代码写错才有的东西。但在游戏测试里死循环反而是个工具。压力测试的时候你想让某个操作一直跑跑到游戏崩溃为止这正是 while True 的用途count 0 try: while True: send_skill(1001, fire_ball) count 1 if count % 100 0: print(f已释放 {count} 次火球术) time.sleep(0.2) except KeyboardInterrupt: print(f手动终止共释放 {count} 次) except Exception as e: print(f游戏崩溃或报错第 {count} 次触发{e})用 try 包裹死循环是非常实用的测试写法。正常情况它一直跑一旦游戏报错、崩溃或者你忍不住按了 CtrlC脚本都能优雅地退出并把当时的循环次数打出来。这个次数就是绝佳的复现条件。但必须提醒一句死循环测试的脚本一定要留“逃生的门”。如果你在环境里跑 while True 脚本又忘了写异常处理整个测试站都会被卡住。我见过有同事把死循环测试挂在 CI 上结果是半夜 CI 卡死第二天早上整个流水线全堵住完全没法构建。3.3 break、continue、else循环的“控制杆”要精细控制循环的行为离不开 break、continue、else 这三个关键词。游戏测试里break 最常见的用途是“找到就停”遍历一个很大的角色列表只要找到了目标角色就没必要继续遍历了target_role_id 1088 for role_id in all_role_ids: if role_id target_role_id: print(f找到目标角色 {role_id}) breakcontinue 则反过来是“跳过不处理”批量校验角色数据的时候有些角色是策划标记为“废案”的不需要校验看到就跳过for role_id in all_role_ids: if role_id in deprecated_roles: continue validate_role_data(role_id)while 循环里的 break 也很有用它通常是循环结束的“出口”之一。比如断线重连的测试客户端一直重连直到连上就 break否则最多循环 30 次retry_count 0 while retry_count 30: if try_connect(): print(重连成功) break retry_count 1 time.sleep(2) if retry_count 30: print(重连失败30次全部超时)else 子句很多人没用过它的特性是循环正常结束没有通过 break 跳出的时候会执行 else 里的代码。这个特性特别适合写“遍历完都没找到目标”的检查逻辑for role_id in all_role_ids: if role_id target_role_id: print(角色存在) break else: print(角色不存在)这一看就比用标志位优雅得多代码少了两行逻辑也更清楚。游戏测试里查“某个物品 ID 是否在配置表里”这个模式几乎是标配写法。4. 循环实战从“跑脚本”到“顶一个人”4.1 案例一自动化耐力测试讲完基础用法来几个真实案例串一串。第一个案例是耐力测试。游戏里有个挂机玩法玩家角色会在地图上自动打怪测试要求是这个挂机玩法连续跑 12 小时不能崩溃、不能卡死、不能掉线。手工测试的话你就得盯着屏幕看 12 小时这根本不现实。用循环来做我们不是盯着看而是每一分钟做一次状态检查循环 720 次。期间如果发现游戏进程消失、CPU 占用异常骤降、或者网络断开立刻记录并退出。import time import psutil game_process_name game_client.exe last_report_time time.time() check_interval 60 # 每 60 秒检查一次 total_duration 12 * 3600 # 12小时 start_time time.time() while time.time() - start_time total_duration: time.sleep(check_interval) running any(p.name() game_process_name for p in psutil.process_iter()) if not running: print(f第 {(time.time() - start_time) / 60:.1f} 分钟时游戏进程消失) break if time.time() - last_report_time 3600: print(f已运行 {((time.time() - start_time) / 3600):.1f} 小时进程正常) last_report_time time.time() else: print(12小时耐力测试通过进程全程存活)注意这里我用了一个 while else 的组合。如果是因为循环条件自然结束也就是顺利跑完 12 小时else 会触发打印通过。如果是中途 break 退出else 不会触发这样测试结果的判定非常清晰。这个脚本的价值在于它把一个需要人盯 12 小时的活变成了一行监控脚本。你该下班下班第二天来直接看输出就行。4.2 案例二伤害浮动校验第二个案例是数值向的。游戏里某个技能的伤害描述是“造成 200% 攻击力的伤害”测试要验证实际伤害是不是真的在对应区间。你不能只看一两次伤害那样样本太小第一次暴击了第二次闪避了得出的结论不可靠。标准做法是打 1000 次木桩统计伤害值的分布import random damage_list [] attack_power 2000 expected_rate 2.0 critical_chance 0.3 critical_multiplier 1.8 for i in range(1000): critical random.random() critical_chance if critical: damage attack_power * expected_rate * critical_multiplier else: damage attack_power * expected_rate # 模拟一点随机波动 damage * random.uniform(0.98, 1.02) damage_list.append(round(damage, 1)) max_damage max(damage_list) min_damage min(damage_list) avg_damage sum(damage_list) / len(damage_list) print(f最高伤害{max_damage}) print(f最低伤害{min_damage}) print(f平均伤害{avg_damage:.1f})for i in range(1000) 这个写法把“重复采样多组数据”这个动作压缩成了一行。你调一下循环次数就可以轻松做到 10000 次采样在手工时代这是不可想象的。有了这组数据你就可以对照策划给的公式判断实际伤害有没有明显偏向某一个区间。之前我遇过一个 bug某个 buff 对技能伤害的加成从 20% 写成了 2%单次测试根本看不出来就是靠这种大量采样的循环才暴露出来的。4.3 案例三测试数据批量生成第三个案例跟数值无关但特别实用批量生成测试账号的初始数据。游戏每次版本更新都要有一批测试账号账号要求是具有指定数量金币、指定注册时间、指定角色等级。以前是手工一条条改数据库或者一封邮件一封邮件地发用循环生成就快多了accounts [] for i in range(50): account { username: ftest_user_{i:03d}, gold: 100000, level: (i % 10) 1, vip_level: i % 5, registered_days_ago: i * 3, } accounts.append(account) for acc in accounts[:5]: print(acc)这里的 ftest_user_{i:03d} 用到了字符串格式化i 会从 0 到 49格式化后是 000 到 049。批量生成测试数据还有个隐藏的用途版本回归测试的时候你要验证 50 个不同等级的账号分别能领到多少月卡奖励手工一个个搭账号要搭到天荒地老这种脚本几秒钟就能搞定。5. 循环嵌套和性能大实话全在这儿5.1 循环嵌套组合测试的高效姿势游戏里很多测试是组合场景比如“每个职业搭配每种武器”都要跑一遍。这种场景就需要循环嵌套classes [战士, 法师, 刺客] weapons [剑, 法杖, 匕首, 弓] for cls in classes: for weapon in weapons: print(f测试组合{cls} {weapon})两层 for 循环的关系是外层跑一次内层完整跑一圈。所以上面这个会输出 3 乘以 4总共 12 个组合。写嵌套循环的时候要注意顺序外层变量的变化频率低内层变量的变化频率高。如果你把顺序写反了输出顺序变了倒还好但如果有一层循环负责启动游戏、另一层负责操作顺序写错就可能启动一次游戏只测了一个组合效率完全不对。还有一种“剪枝”的写法值得学。组合测试的盲区是很多组合其实没必要跑全量策划会指定某些职业不能装备某些武器比如法师不能装备大剑。你可以在内层循环里加 continuefor cls in classes: for weapon in weapons: if cls 法师 and weapon 大剑: continue print(f测试组合{cls} {weapon})理论上 12 个组合变成了 11 个。这是真实做组合测试常用的手法遍历所有可能性但是用规则把无效组合过滤掉比一开始就去构造“有效组合列表”要简单很多因为你不需要维护那份列表只需维护过滤规则就好。5.2 新手最常踩的循环理解误区循环这个语法简单但真正写起来新手很容易踩一些微妙的坑。第一个是“死循环”的入门版——条件写反了或者根本没有触发方式while True: print(一直跑) # 没有退出条件也没有 break第二种是“一重循环里修改正在遍历的列表”这个比较隐蔽items [1, 2, 3, 4] for item in items: if item % 2 0: items.remove(item)看起来没什么问题但运行之后你会发现结果不对——因为列表在遍历过程中被修改索引错位导致某些元素被跳过。在游戏测试里你可能是在遍历关卡列表的同时又往里塞新的关卡数据那结果就乱了。正确做法是拷贝一份再遍历items [1, 2, 3, 4] for item in items[:]: if item % 2 0: items.remove(item)第三个是“break 和 continue 放错位置”。continue 会让当前这次循环的剩余代码全部跳过如果你 continue 写得太靠前后面的代码永远执行不到。游戏测试里最典型的是你循环检查多个指标帧率、内存、加载时间continue 不小心写早了后半段指标全跳过了你还在纳闷为什么内存检查从来没执行过。5.3 循环性能的“大实话”写循环的时候性能问题也是绕不开的。给新手三句大实话。第一句不要把跟循环无关的操作放进循环里。比如你循环读一个很大的配置列表其实整个列表读一次就够了放循环里等于每次读一遍白白多出几百倍的 IO。正确姿势是列表从文件里读一遍存成变量循环里只用这个变量。第二句循环里尽量别做大字符串拼接。每次拼接实际上都会生成新的字符串对象循环几百次感觉不明显循环几万次性能就肉眼可见地变慢了。游戏测试里经常要打印大量运行日志用列表收集再 join比 拼接快得多log_lines [] for i in range(10000): log_lines.append(f第 {i} 次测试记录) full_log \n.join(log_lines)第三句列表推导式是你该学的第一个“高级语法”。很多简单的 for 循环完全可以用列表推导式替换代码更短性能也略好# 普通写法 squares [] for i in range(100): squares.append(i * i) # 列表推导式 squares [i * i for i in range(100)]这两种写法结果完全一样但列表推导式在 Python 社区里是更「地道」的风格。你平时自己写测试脚本可以随意但如果你的脚本要给团队其他人 review用列表推导式会显得专业很多。6. 循环调试与常见问题实录6.1 一段脚本跑不出预期结果先查这几处写循环脚本跑不出预期结果是常态。我发现几个高频原因优先排查它们会省很多时间。第一处缩进问题。Python 用缩进划分代码块for 循环体下面少缩进或者多缩进程序的行为就完全变了。最典型的错误是把 print 和 for 对齐了结果循环只打印了最后一次结果。第二处range 的边界。range(10) 是 0 到 9不包含 10。想要 1 到 10 就得写 range(1, 11)。这个细节在生成测试数据的时候特别容易坑人——你想生成 10 条数据结果循环只有 9 条因为 range(10) 从 0 开始到 9 结束。第三处浮点数和整数混淆。循环变量 i 是整数但如果是做除法统计记得把其中一个操作数转成 floatavg total / len(items)在 Python 3 里结果就是浮点数一般没问题但如果是在 Python 2 的老项目里两个整数除会得到整数这个坑有年代感但还有人踩。第四处外部函数返回值为空。测试环境不稳定你调 get_game_state() 偶尔会返回 None然后你在循环里直接拿 None 的属性一跑就报错。这时候要判断并跳过或者给它一个默认值game_state get_game_state() if game_state is None: continue6.2 快速定位循环卡死的方法脚本跑着跑着不动了这是每个测试同学都遇到过的。第一个要查的是 sleep 是不是写太大了。time.sleep(5) 有时候你觉得只等了 2 秒人会不耐烦然后手动去重启进程其实脚本原本是正常的。第二个要查的是有没有“跑不完的循环终结条件”。最常见的 bug 是 while 里面更新了局部变量但 while 条件里判断的却是另一个变量永远不满足就成了死循环。写 while 的时候要多看一眼条件里用的变量在循环体内到底有没有被更新。第三个要查的是“集合不断增大”。循环里往 list 追加数据又没限制长度内存会慢慢涨上去。游戏测试的长时间压测脚本最容易出这个问题。我后来习惯在循环里定期打印内存占用情况一旦发现异常增长就能及时定位到“是循环内哪一行在不停追加数据”。6.3 常见循环问题速查表现象可能原因快速解法只输出最后一次结果print 缩进在 for 外面将 print 移入循环体也就是让它与循环内其他语句同级缩进循环没执行条件一开始就不成立 / range 边界写错打印 range 或条件变量确认边界死循环不退出while 条件内变量未更新检查循环体内有没有修改条件变量循环中途报索引错误遍历中包含已删除元素用切片副本 items[:] 替换原 list结果跟手工算的不一样嵌套循环顺序理解错在外层循环开头打印外层变量值对照逻辑列表推导式运行超预期表达式写错 / 嵌套推导式顺序颠倒单测几组小数据验证中文输出乱码控制台编码问题脚本开头加 # -- coding: utf-8 --或设置 PYTHONIOENCODINGutf-8这个速查表是我整理自己平时遇到问题的高频组合不一定覆盖所有情况但能覆盖掉八成。7. 把循环写进测试习惯给新手的三点经验这块讲讲我个人这几年在游戏测试里反复用循环的一些心得。第一点循环脚本的复用性远比一次性跑通更重要。你为了某个版本临时写的一个循环脚本不要用完就删把它整理成固定函数。比如我上面写的 get_role_gold 校验、耐力测试、伤害浮动分析都留在了团队的脚本库里。下一次有类似需求改几个参数就能直接跑省的时间是成倍的。第二点循环结构的脚本想办法让它“输出友好”。不要只在循环里写 print 完事最好能把关键数据收集起来最后统一汇总报告。比如伤害统计那个案例如果你只打印每一次的伤害1000 行全是数字肉眼根本看不出规律用循环收集起来再做 min、max、avg报告才有价值。第三点循环不是万能的适当的时候要跳出循环思维。有些测试场景虽然可以用循环但更适合用现成的测试工具。比如 UI 自动化真正的循环操作写在 pytest 框架里配合参数化(data-driven)来跑比裸写 for 更优雅。循环语言本身要熟练但到了框架层面学会“框架帮你管理循环”也是进阶的必经之路。我在实际项目中还发现一个高频需求循环运行的结果要能自动生成日志文件方便历史追溯。这个虽然不复杂但很值得早点做进自己的脚本模板里。每次跑完循环自动把时间、循环次数、异常信息都写进一个 log 文件之后哪天要排查问题翻日志比拍脑袋回忆靠谱多了。最后再分享一个小技巧。真遇到“循环逻辑怎么都想不通”的时候别硬想插几个临时打印变量看中间结果。很多新手把时间耗在盯着代码干看其实打印几行中间值一跑就明白数据流在哪个环节断掉了。这一个习惯能让你的调试效率提升一半以上。