
1. 准备入坑为什么游戏测试工程师必须学JSON干游戏测试这行每天打交道最多的不是游戏画面而是数据。你测试一个副本掉落填表工具导出的是一串嵌套结构你验证抽卡概率日志文件里全是键值对你负责接口自动化包体和响应体几乎都是JSON格式。可以说JSON是游戏项目里的“通用语言”从策划配表、服务端协议、日志埋点到客户端本地存档处处都是它的影子。我带的几个新人测试刚接触游戏项目时最懵的一件事就是打开配置文件满屏的{}和[]完全不知道从哪里下手对着接口文档也搞不清返回体里那段嵌套结构到底怎么取值。这8周训练营做到第5周我就专门安排了一整天来讲JSON数据处理也就是你看到的这个“501 JSON数据处理”章节。这篇博文我把当天的完整内容和实战经验整理出来希望能帮你少走弯路。这篇内容适合三类人一是刚入行想往游戏测试方向走的同学二是已经在做功能测试、想转接口自动化或性能测试的测试工程师三是工作中经常要和配置文件、日志数据打交道的技术运营或开发新手。我会从JSON语法基础讲到Python处理JSON的核心API再到游戏测试中最常用的数据驱动玩法最后用一整套真实踩坑记录收尾。跟着走一遍你就能在项目里直接上手。2. 先搞清楚JSON的本质不是格式是数据结构2.1 JSON到底是什么跟字典、数组有什么关系很多人学JSON时卡在一个点上它到底是一种格式还是一种数据类型我换个说法你就明白了——你在Python里写的字典{name: 野怪, hp: 100}跟一段JSON文本{name: 野怪, hp: 100}长得几乎一样但本质不同。Python字典是内存中的数据结构而JSON是文本是用于储存和传输的字符串。也就是说游戏服务器返回给你的响应体本质上是一大段字符串你在客户端看到那个结构是因为框架已经帮你把字符串解析成字典了。这个“解析”的过程就是程序员常说的“序列化”和“反序列化”序列化把Python的字典、列表转成JSON字符串方便写入文件或通过网络传输反序列化把收到的JSON字符串转成Python的字典、列表方便程序读取和操作打个比方这就跟寄快递一样。你家里买的书架Python字典不能直接搬上货车得拆成零件JSON字符串装箱到了目的地再按说明书组装起来解析回字典。所以你在游戏里看到的“读取存档”“同步掉落数据”底层都是在做这套序列化和反序列化。游戏测试中之所以到处是JSON而不是XML核心原因有四点对比项JSONXML阅读体验结构紧凑一眼看懂层级标签冗余要看半天解析开销轻量解析速度快较重占CPU和内存数据表达天然支持数组和对象嵌套需要额外定义节点关系传输体积小省带宽大多出一堆标签对游戏这种高实时、高吞吐的场景来说响应速度和体积大小直接影响体验所以JSON成了绝对主流。2.2 JSON的四种合法数据类型别被花括号吓住JSON里能写的类型非常有限总共就四类比Python少多了对象用大括号{}包裹里面是一组键值对。键必须是双引号包裹的字符串值可以是任意合法JSON类型数组用中括号[]包裹里面是一组值用逗号分隔字符串双引号包裹支持转义字符数值整数或浮点数没有单引号字符串这个概念布尔值true和false注意全是小写空值null这里必须敲黑板强调一个新手高频坑JSON里不允许用单引号不允许有注释键必须是双引号。我见过太多测试同学把Python里写字典的习惯带过来写出一串带单引号的“JSON”结果解析直接报错。给你看一份游戏掉落配置的JSON示例模拟的是手游里一个副本BOSS的掉落表{ bossName: 暗影领主, level: 50, dropList: [ {itemId: 1001, itemName: 青铜剑, probability: 0.6}, {itemId: 1002, itemName: 紫金甲, probability: 0.3}, {itemId: 1003, itemName: 传说宝珠, probability: 0.1} ], respawnTime: 300, isOpen: true, rareDrop: null }看到没dropList是一个数组数组里每个元素又是一个对象isOpen是布尔值rareDrop是空的。这就是JSON的嵌套之美——你可以用几层简单的组合表达出任意复杂的游戏数据。2.3 校验JSON格式的正确姿势写配置文件、翻接口返回值经常遇到“格式错在哪”的问题。肉眼检查不仅慢还容易漏。我自己最常用的工具组合是编辑器插件VS Code装一个Prettier保存时自动格式化JSON缩进乱了立刻能看到在线校验把内容粘贴到JSONLint这类工具它会精确到第几行第几个字符报错Python命令项目中临时校验可以起一个Python终端用json.loads()直接跑报错信息带行列号真到了后期做自动化我习惯把“合法性校验”直接写进测试用例里比如写一个参数化用例专门喂各种畸形JSON给接口验证服务端能不能正确返回400错误码。这才是游戏测试该有的思维方式把工具当测试对象用自动化代替人肉检查。3. Python处理JSON的核心API半小时吃透3.1 json模块的四个主角loads、dumps、load、dumpPython标准库的json模块是处理JSON数据的地基。很多第三方库比如requests、pytest底层都在用它。你只需要掌握四个函数就能覆盖95%的工作场景函数方向作用对应场景json.loads()字符串 → 对象JSON字符串解析成Python字典/列表处理接口返回的文本json.dumps()对象 → 字符串Python字典/列表转成JSON字符串构造请求体、打印日志json.load()文件 → 对象读取JSON文件直接得到Python字典/列表读取配置文件、测试数据json.dump()对象 → 文件Python对象直接写入JSON文件保存测试结果、写配置文件先看两个最常用的loads和dumpsimport json # 字符串 - 字典 response_body {code: 0, message: success, data: {gold: 1000}} data json.loads(response_body) print(data[data][gold]) # 输出 1000 # 字典 - 字符串 new_config {bossName: 暗影领主, level: 50} json_str json.dumps(new_config) print(json_str) # {bossName: 暗影领主, level: 50}这段代码值回票价的点在于loads之后你就能用data[data][gold]这种链式取值的写法直接深入到任意层级的数据里。游戏日志里最常见的就是这种嵌套结构比如“玩家操作记录”{userId: 123, action: click, detail: {itemId: 1001, count: 2}}熟练取嵌套层级的路径是测试脚本的基本功。再看dump和load这两个是处理文件的非常适合做测试数据管理import json # 字典 - 文件 config { server: {host: 192.168.1.10, port: 8080}, retryCount: 3 } with open(server_config.json, w, encodingutf-8) as f: json.dump(config, f, ensure_asciiFalse, indent2) # 文件 - 字典 with open(server_config.json, r, encodingutf-8) as f: loaded_config json.load(f) print(loaded_config[server][host])注意我打开文件时都指定了encodingutf-8这一点在Windows上尤其重要不指定的默认编码是gbk最容易踩中文乱码的坑。3.2 dumps的ensure_ascii和indent参数两个必记参数用dumps把中文转成JSON字符串时新手经常会遇到一个诡异现象输出的中文全变成了\u5f71\u77f3这样的一串反斜杠加字母。这不是数据坏了而是ensure_ascii参数默认为TruePython为了保证ASCII兼容把所有非ASCII字符都做了Unicode转义。测试脚本里处理中文游戏名、道具名、玩家昵称的时候这种输出完全没法看。解决办法就是传ensure_asciiFalseimport json data {playerName: 夜风, guild: 星辰公会} print(json.dumps(data, ensure_asciiFalse)) # {playerName: 夜风, guild: 星辰公会}另一个参数是indent控制输出缩进。JSON默认的dumps输出是一行挤到底的日志短还好数据一长眼睛就瞎了。加上indent2输出立刻变得层级分明print(json.dumps(data, ensure_asciiFalse, indent2))这里给个实操建议在游戏项目的日志处理器里统一封装一个format_json(obj)函数内部固定用json.dumps(obj, ensure_asciiFalse, indent2)约定所有测试脚本打印JSON都走这个函数。别小看这个手感差异排查数据问题时格式规整的日志能省一半时间。3.3 处理JSON数组当成Python列表来遍历游戏测试中高频出现的JSON结构之一就是数组比如排行榜、掉落列表、背包物品。JSON数组对应Python的列表取值靠下标遍历靠for循环import json rank_data {game: 热血江湖, rankList: [{rank: 1, name: 剑神, score: 99999}, {rank: 2, name: 刀狂, score: 88888}]} data json.loads(rank_data) for player in data[rankList]: print(player[rank], player[name], player[score]) # 1 剑神 99999 # 2 刀狂 88888这里有一个判断技巧拿到一段JSON后第一步不是急着取值而是先判断最外层是{}还是[]。如果是{}说明是单个对象用键取值如果是[]说明是数组得用下标或遍历。很多测试脚本一报错TypeError: string indices must be integers就是因为拿处理字典的方式去处理了列表。4. 游戏测试实战把JSON变成用例数据源4.1 用JSON文件管理测试数据告别硬编码我见过太多测试工程师写脚本把测试数据直接怼进代码里data {username: test001, password: 123456, level: 99}这种硬编码的问题很明显换个账号就得改代码测一个多条件场景就得复制十几遍。在游戏测试里数据往往是几十上百条比如需要验证不同等级玩家的战斗结算、不同VIP等级的礼包领取规则。更好的做法是把测试数据独立成JSON文件然后用读取文件的代码去加载。假设我们要测试一个签到系统的接口正常情况下的测试数据放在sign_data.json里[ {caseName: 普通玩家签到, userId: 10001, signDay: 3, expected: 0}, {caseName: 月卡玩家签到, userId: 10002, signDay: 15, expected: 0}, {caseName: 未登录签到, userId: -1, signDay: 1, expected: 1001}, {caseName: 重复签到, userId: 10001, signDay: 3, expected: 2002} ]然后写一个加载函数把JSON读成Python列表import json def load_test_data(file_path): with open(file_path, r, encodingutf-8) as f: return json.load(f) cases load_test_data(sign_data.json) for case in cases: print(case[caseName], case[expected])这样做的收益是立竿见影的用例数据和执行代码彻底分离测试同学可以只维护JSON文件不需要碰Python代码新增一条用例往文件里加一行就行回归测试跑起来的时候框架能用一份数据源驱动所有接口用例。这就是游戏测试里常说的数据驱动测试。4.2 pytest结合JSON做参数化批量跑接口用例如果你在项目里用pytest那JSON数据驱动会更爽因为pytest的parametrize装饰器天然支持传入列表而列表正好可以从JSON文件加载import json import pytest import requests def load_cases(): with open(sign_data.json, r, encodingutf-8) as f: return json.load(f) pytest.mark.parametrize(case, load_cases()) def test_sign(case): payload { userId: case[userId], signDay: case[signDay] } resp requests.post(http://game-server/api/sign, jsonpayload) result resp.json() assert result[code] case[expected]这里requests.post的jsonpayload参数内部就是调用了json.dumps直接把字典序列化成JSON字符串发送。而resp.json()则是在loads基础上的封装一行代码把响应体解析成字典。跑起来之后pytest会为JSON文件里的每条数据生成一个独立的测试用例成功失败一目了然日志里还带有参数内容定位问题特别方便。这也是我推荐游戏测试团队做接口自动化的起步方案不需要复杂框架pytest加一个JSON文件就能撑起一套可维护的接口回归用例集。4.3 用jsonpath提取嵌套数据比手写循环更省心接口返回的数据往往层级很深比如一个完整的登录接口返回可能是data.user.info.level这种三层嵌套。用Python原生语法写data[data][user][info][level]虽然能行但每取一个值都要写一大串。而且遇到列表套字典、字典套列表的复杂结构手写循环非常容易出错。这时候我推荐用jsonpath库它的语法很像Linux文件路径专门用来从JSON中定位数据。比如import json from jsonpath import jsonpath response { code: 0, data: { user: { info: {level: 50, vip: 3}, items: [ {itemId: 1001, count: 5}, {itemId: 2002, count: 2} ] } } } # 获取玩家等级 level jsonpath(response, $.data.user.info.level)[0] print(level) # 50 # 获取所有itemId item_ids jsonpath(response, $.data.user.items[*].itemId) print(item_ids) # [1001, 2002]$代表根节点*代表通配所有元素。这种写法对测试脚本特别友好尤其是断言接口返回时你想验证“返回的奖励列表里是否包含某件道具”直接用jsonpath提取出所有itemId再判断1001在不在里面比写多层循环高效多了。需要提醒一句如果列表里压根没有匹配的数据jsonpath会返回False而不是空列表。这个返回逻辑和Python的if not list判断有区别用的时候要先确认有值再取[0]否则容易踩IndexError的坑。5. 常见报错与踩坑实录这些坑我都替你踩过5.1 编码问题中文乱码和\u转义游戏项目里最不缺的就是中文道具名、服务器名、玩家昵称全是。在处理JSON时编码相关的坑基本有两类第一类是文件读写乱码。表现是打开JSON文件显示一堆“锟斤拷”或者load()直接抛UnicodeDecodeError。原因几乎都是没指定编码。解决办法也是唯一的打开文件时统一加encodingutf-8。在Windows上开发时尤其要养成这个习惯。第二类是打印日志时中文变\u。这种情况是ensure_ascii默认值造成的打印和写文件时手动传ensure_asciiFalse中文就正常了。实际项目中我习惯统一封装不然后面每个脚本都写一遍参数容易漏。5.2 格式问题为什么json.loads报错json.loads()报错的原因五花八门但九成以上是下面这几种我列成速查表报错场景典型错误信息原因解决办法用了单引号Expecting property name enclosed in double quotesJSON规定键和字符串必须双引号把所有单引号换成双引号多了尾逗号Expecting valueJSON不允许最后一个元素后有逗号删掉最后一项后面的逗号有注释Expecting property name enclosed in double quotesJSON不支持#或//注释解析前用正则去掉注释行空文件/空字符串Expecting value: line 1 column 1loads()传入空内容先判断内容非空再解析混入BOM头Unexpected UTF-8 BOM文件开头有不可见字符用utf-8-sig编码打开这里重点说一下尾逗号它是最隐蔽的。很多编辑器格式化代码时不会帮你检查JSON里的尾逗号而你在Python里写列表习惯了结尾加逗号一顺手就写进JSON里了。格式看起来没毛病一跑就报错。另一个隐蔽坑是Windows上的记事本保存JSON文件时会给文件加BOM头。json.load()默认不认识BOM会报奇怪的编码错误。解决办法是打开文件时改成encodingutf-8-sig这个编码会自动跳过BOM头。5.3 类型问题布尔值、null和数字的陷阱JSON里的true、false、null和Python里的大写True、False、None长得不一样。很多测试脚本从JSON拿到值之后直接和Python的真假值比较就会出现“明明日志里是个false代码里if判断却是真”的怪事。看这个典型例子import json resp {isOpen: false} data json.loads(resp) print(data[isOpen] is False) # True这里是对的 # 但如果有人手抖写成 print(data[isOpen] False) # True其实也行其实json.loads在解析时就主动把JSON的false转成了Python的False这个转换是自动的不需要你操心。真正容易出问题的是你在手写JSON测试数据时把false写成了False这时候要么用dumps转回字符串时会报错要么直接把数据写坏。所以在JSON文件里一定要写小写true/false/null。数字方面也有一个坑是浮点数精度。游戏里掉落概率、伤害百分比经常是浮点数JSON解析后Python在打印时可能出现0.30000000000000004这种样子这是因为浮点数在内存里的二进制表示不精确。做断言的时候不能直接比较浮点数要使用round()或math.isclose()result json.loads({probability: 0.3}) assert round(result[probability], 1) 0.35.4 性能问题超大的JSON文件怎么处理有的游戏日志JSON动辄几十上百MB比如一周的全服战斗记录。如果用json.load()一次性读入内存轻则卡死机器重则直接内存溢出。这种场景下处理方式有两个方向第一个方向是流式解析用ijson这类库逐条读取顶层数组里的每个元素不需要把全部数据一次性放进内存import ijson with open(battle_log.json, r, encodingutf-8) as f: for record in ijson.items(f, item): # 逐条处理战斗记录 process_battle_record(record)第二个方向更贴近日常预处理后再加载。比如先写个脚本把大JSON按日期拆成小文件或者用grep/Python脚本先过滤出需要的字段生成一份精简JSON再给测试用例用。大多数游戏测试不需要动辄分析上G流量把数据先洗一遍再喂给测试脚本性价比更高。我自己实际跑过一遍的体会是性能问题永远比格式问题少但一旦遇到就是大问题。测试环境机器配置一般先把大数据文件拆小是共识。5.5 模块和依赖requests、pytest、jsonpath的安装这篇博文里的示例代码依赖三个第三方库requests、pytest、jsonpath。它们都不是Python标准库需要先安装。装完后可以用下面的命令验证pip install requests pytest jsonpath python -c import requests, pytest, jsonpath; print(ok)如果你在公司内网环境下建议优先配置国内镜像源否则下载速度可能慢到怀疑人生。命令格式是这样pip install requests pytest jsonpath -i https://pypi.tuna.tsinghua.edu.cn/simple我在新人带教时发现一个高频问题有人Windows上装了多个Pythonpip install装到一个解释器python命令却是另一个解释器在跑于是import requests永远报ModuleNotFoundError。排查方式也很简单在终端里分别执行pip --version和python --version看看两个命令指向的路径是不是同一个Python环境。如果是要么用python -m pip install来安装要么直接配置虚拟环境一劳永逸。6. 游戏测试工程师的进阶心法学完JSON之后做什么掌握JSON数据处理只是8周通关计划里的一个节点。按我的课程安排第5周你练完这套JSON基本功后面紧接着要做三件事把JSON能力用在接口自动化的请求构造和响应断言里把接口自动化跑出来的结果数据用JSON格式回写到报告文件里最后再用pytest把这些用例组织成一套可持续回归的测试工程。这三件事里最核心的思维转变是不再把JSON当作“要处理的数据”而是把它当作“组织测试的骨架”。举个例子你可以在工程目录下建一个test_data文件夹专门放各种JSON测试数据文件建一个output文件夹专门放每次跑完测试的JSON结果再建一个config文件专门放环境地址和账号信息。整个测试工程从数据到结果全部用JSON串起来。这样团队协作时每个人只需要维护自己负责的那块JSON不需要互相改代码。我在带新人时经常说一句话测试工程师和普通玩家的分水岭不是手速而是看你能不能在数据层面理解游戏。一个副本掉落的配置在你眼里是一堆数值在资深测试眼里是参数组合、边界值、异常输入和自动化用例的原始素材。JSON就是你打开数据视角的第一把钥匙。这篇内容里所有示例代码你在本地都能跑得通。找一份自己游戏项目里的真实JSON配置照着文中几个函数过一遍再试着写一个从JSON文件加载用例的pytest脚本。只要半天你就能从“看到JSON就头大”变成“看到JSON就想怎么拆”。之后再做接口测试、日志分析、测试数据管理都会顺很多。