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,核心原因有四点:
| 对比项 | JSON | XML |
|---|---|---|
| 阅读体验 | 结构紧凑,一眼看懂层级 | 标签冗余,要看半天 |
| 解析开销 | 轻量,解析速度快 | 较重,占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、dump
Python标准库的json模块,是处理JSON数据的地基。很多第三方库(比如requests、pytest)底层都在用它。你只需要掌握四个函数,就能覆盖95%的工作场景:
| 函数 | 方向 | 作用 | 对应场景 |
|---|---|---|---|
json.loads() | 字符串 → 对象 | JSON字符串解析成Python字典/列表 | 处理接口返回的文本 |
json.dumps() | 对象 → 字符串 | Python字典/列表转成JSON字符串 | 构造请求体、打印日志 |
json.load() | 文件 → 对象 | 读取JSON文件直接得到Python字典/列表 | 读取配置文件、测试数据 |
json.dump() | 对象 → 文件 | Python对象直接写入JSON文件 | 保存测试结果、写配置文件 |
先看两个最常用的,loads和dumps:
import 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", encoding="utf-8") as f: json.dump(config, f, ensure_ascii=False, indent=2) # 文件 -> 字典 with open("server_config.json", "r", encoding="utf-8") as f: loaded_config = json.load(f) print(loaded_config["server"]["host"])注意我打开文件时都指定了encoding="utf-8",这一点在Windows上尤其重要,不指定的默认编码是gbk,最容易踩中文乱码的坑。
3.2 dumps的ensure_ascii和indent参数,两个必记参数
用dumps把中文转成JSON字符串时,新手经常会遇到一个诡异现象:输出的中文全变成了\u5f71\u77f3这样的一串反斜杠加字母。这不是数据坏了,而是ensure_ascii参数默认为True,Python为了保证ASCII兼容,把所有非ASCII字符都做了Unicode转义。
测试脚本里处理中文游戏名、道具名、玩家昵称的时候,这种输出完全没法看。解决办法就是传ensure_ascii=False:
import json data = {"playerName": "夜风", "guild": "星辰公会"} print(json.dumps(data, ensure_ascii=False)) # {"playerName": "夜风", "guild": "星辰公会"}另一个参数是indent,控制输出缩进。JSON默认的dumps输出是一行挤到底的,日志短还好,数据一长眼睛就瞎了。加上indent=2,输出立刻变得层级分明:
print(json.dumps(data, ensure_ascii=False, indent=2))这里给个实操建议:在游戏项目的日志处理器里,统一封装一个format_json(obj)函数,内部固定用json.dumps(obj, ensure_ascii=False, indent=2),约定所有测试脚本打印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", encoding="utf-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", encoding="utf-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", json=payload) result = resp.json() assert result["code"] == case["expected"]这里requests.post的json=payload参数,内部就是调用了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。原因几乎都是没指定编码。解决办法也是唯一的:打开文件时统一加encoding="utf-8"。在Windows上开发时尤其要养成这个习惯。
第二类是打印日志时中文变\u。这种情况是ensure_ascii默认值造成的,打印和写文件时手动传ensure_ascii=False,中文就正常了。实际项目中我习惯统一封装,不然后面每个脚本都写一遍参数,容易漏。
5.2 格式问题:为什么json.loads报错
json.loads()报错的原因五花八门,但九成以上是下面这几种,我列成速查表:
| 报错场景 | 典型错误信息 | 原因 | 解决办法 |
|---|---|---|---|
| 用了单引号 | Expecting property name enclosed in double quotes | JSON规定键和字符串必须双引号 | 把所有单引号换成双引号 |
| 多了尾逗号 | Expecting value | JSON不允许最后一个元素后有逗号 | 删掉最后一项后面的逗号 |
| 有注释 | Expecting property name enclosed in double quotes | JSON不支持#或//注释 | 解析前用正则去掉注释行 |
| 空文件/空字符串 | Expecting value: line 1 column 1 | loads()传入空内容 | 先判断内容非空再解析 |
| 混入BOM头 | Unexpected UTF-8 BOM | 文件开头有不可见字符 | 用utf-8-sig编码打开 |
这里重点说一下尾逗号,它是最隐蔽的。很多编辑器格式化代码时不会帮你检查JSON里的尾逗号,而你在Python里写列表习惯了结尾加逗号,一顺手就写进JSON里了。格式看起来没毛病,一跑就报错。
另一个隐蔽坑是Windows上的记事本保存JSON文件时会给文件加BOM头。json.load()默认不认识BOM,会报奇怪的编码错误。解决办法是打开文件时改成encoding="utf-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", encoding="utf-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上装了多个Python,pip 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就想怎么拆”。之后再做接口测试、日志分析、测试数据管理,都会顺很多。