詹聪聪:游戏接口测试自动化实践 · 测试之家
来 testhome10 天了,发了两篇质量保证的水文,全是理论,有些读者老爷希望我来点干货。那今天就发一个干货,这是我测试三年来关于游戏接口自动化测试方面的心血,必定全网唯一,希望大家多多支持,多多点赞!(长文预警)
1.背景
1.0 作者背景
先说一下我的背景,上海某游戏公司 QA,3 年游戏测试经验,虽然工作时间不长,但是接手过一个大型的页游项目和一个在研手游项目,对接口自动化方面有一定的思考和技术沉淀。
1.1 游戏测试行业的现状
国内的游戏测试行业目前普遍的是功能测试走天下,偶尔出来个 UI 自动化测试就了不得了。众所周知,UI 自动化开发和维护成本高,投入性价比很低,而游戏又是一种界面千奇百怪又元素布局纷繁复杂的软件,UI 自动化更是得不偿失,即使上马了自动化测试项目,往往都是虎头蛇尾,在维护方面难以为继。
据我的观察,游戏测试应该是落后外面 5 年的技术沉淀。为什么其它互联网企业,自动化测试满天飞,游戏测试行业依然如此原始。可能游戏行业已经被踢出互联网行业,划归为传统软件开发领域了吧。
1.2 接口自动化测试的难点
虽然我们游戏行业落后,但是我们游戏也有接口可以测试啊!既然可以接口测试,干嘛不自动化呢?提高测试效率,多好的事儿啊。好是好,但没人做啊。
1.3.1 人员素质游戏测试薪酬普遍比同行低几千,基本上招的都是黑盒测试,技术能力薄弱,接口测试还能搞搞,你让他们开发接口自动化平台,抱歉,没那个能力。所以游戏测试做到最后,并不是成为技术大牛,大部分转游戏策划了。毕竟游戏是我测的,我还不了解嘛,照葫芦画瓢也能写个策划案出来。
1.3.2 通信协议特殊对于大部分的游戏而言,客户端和服务端交互,都是基于 TCP 协议自定义传输协议。划重点,用的不是 HTTP 协议!接口测试所需的数据的发送、接收和验证功能都必须建立在:有一个完备的游戏客户端的基础之上,完备性体现在能够正确接收和处理异步消息(包括服务端的推送消息和接口返回消息)。
1.3.3 没有测试工具由于通信协议的特殊性,相当一部分(只支持 HTTP 协议的)接口测试工具是没办法用的。所以游戏测试人员测接口,基本上都是各个公司开发人员编写的发包工具来测试的。开发大哥给你个测试工具就不错了,你还想怎么样?什么?要写个自动化测试工具?一边凉快去,没看到我正忙着嘛!
1.3.4 没有公司的支持对于老板来说,游戏出 bug 有什么大不了的,给玩家补偿就好了,我要的是游戏能尽快上线赚钱,其它的一律靠边站。你让我开高工资招测试开发,还要花时间搞这些花里胡哨的东西,那是不可能的
另一方面,公司的预算,更倾向于原画、策划、前端,原画可以画皮肤卖,策划可以想玩法套路,前端可以把东西做出来,怎么说都是性价比更高。
1.3.5 其他其实还有其他方面的不利因素,比如:立项时就没有考虑到以后要做接口自动化,设计的时候也不会在给予便利;没有 HTTP 协议的那种 restful API,没有系统化结构化的接口设计,测一个 case 要有一套复杂的前置动作等。(但是我觉得那都不是主要原因)
1.3 实现自动化的意义
既然上面有一堆不利因素,你为什么还要去做接口的自动化?
一个很重要的现实因素:因为项目组就我一个 QA,我不做自动化,意味着每次更新,我都要手动去做回归测试。程序员最讨厌的事情是什么:重复!当我做了实现自动化,我可以把节省下来的时间用来喝咖啡啊,多好!
还没怎么介绍干货呢,废话倒是讲了一堆,下面要进入正题。
2.做一个游戏客户端
理论上这个时候要讲接口自动化测试框架设计,但是现在连接口测试工具都没有,也谈不上设计自动化测试框架,先写个游戏客户端作为测试工具吧。
2.1 游戏客户端架构设计
2.2 做一个 socket 客户端
2.2.1socket 客户端这个好做,随便一个编程语言,都有网络编程,你照着例子,你就能写出一个 socket 客户端。以 python 为例:
import socket class Client(object): def __new__(cls, playerId, game_server, certificate): cls.playerId = playerId cls.game_server = game_server cls.certificate = certificate cls.cache = b"" return super(Client, cls).__new__(cls) @classmethod def server_connect(cls, server): sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) sock.connect(server) return sock def receive(self, sock): pass def send(self, *args, **kwargs) pass 2.2.2 数据封装
我们知道 socket 数据传递的形式是二进制数据流,一般接口数据是以 json 格式为主,这就涉及到了数据的封装。简单来说就是把 json 数据转化为字符串,然后再通过封装成二进制数据流。
对于数据封装,就涉及到了各个公司自定义的通信协议格式。我们自定义一个协议(对于客户端而言),收发的数据报文的格式如下:
假如我要发送一个接口,数据如下:
""" 服务器id:10203 接口名称:store@buy 数据包id:5 数据内容:giftId=7392&useTicket=1&ticketId=357860382 """ import struct serverId = 10203 command = "store@buy".encode("utf-8") packId = 5 packData = "giftId=7392&useTicket=1&ticketId=357860382".encode("utf-8") packLength = 40 + len(packData) # 最终发送的数据流 data = struct.pack(">ii32si{0}s".format( len(packData)), packLength, serverId, command, packId, packData)
接收服务端的推送就可以这样解析:
""" 接口名称:store@buy 数据包id:5 状态码:200 数据内容:{msg:"purchase success!"} """ import struct # 假设接收到的数据流如下 cache = b'x00x00x00Cstore2@buyx00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x00x05x00x00x00xc8{msgxefxbcx9a"purchase success!"}' packDataLength = len(cache) - 40 # 接收数据流 struct.unpack(">32sii{0}s".format(packDataLength), cache)
这里的状态码可以我使用了 http 的状态码,你也可以自定义一套。
2.2.3 数据压缩解压缩前面的数据都是没有经过压缩的,现在压缩是数据传输的标配,一方面是为了节省带宽,另一方面是为了节省磁盘消耗。
在 python 中,二进制数据流压缩使用 zlib 库。对于我们的通信协议而言,值得压缩的数据,就是数据主体部分。
import zlib packData = zlib.compress(original_data) original_data = zlib.decompress(packData) 2.2.4 数据加密
大部分游戏数据不需要加密,但是特殊场景下也是需要数据加密的,比如登陆或者支付等场景。常规的非对称加密思路如下:
客户端和服务器端建立连接 客户端产生非对称密钥,将公钥传送给服务器端 服务器端通过公钥将密钥进行加密并传送给客户端 客户端接收到密钥并进行解密,双方开始通信类似地,数据加密针对的是数据包体的部分,在 python 中使用 rsa 库对这部分二进制数据加密。
import rsa def rsa_encrypt(string): (public_key, private_key) = rsa.newkeys(512) encrypt_string = rsa.encrypt(content, public_key) return (encrypt_string, private_key) def rsa_decrypt(string, public_key): decrypt_string = rsa.decrypt(string, public_key) return decrypt_string
2.3 接口层
2.3.1 接口定义接口定义在 Command 类中,单独的方法,参数一目了然,方便调用,也有利于拓展。
class Command(object): @staticmethod def store_buy(serverId, client, packId, giftId, useTicket, ticketId): client.send(serverId, "store@buy", packId, giftId=giftId, useTicket=useTicket, ticketId=ticketId) @staticmethod def store_info(serverId, client, packId): client.send(serverId, "store@info", packId) commands = Command() 2.3.2 接口处理
自定义一个 Handler 类处理服务端推送消息处理器。work 是事务处理方法,test 是测试时的数据验证方法。
class Handler(object): def __init__(self, robot): pass def work(self, robot, response_data): """ 事务用处理器 """ pass def test(self, robot, response_data): """ 测试用处理器 """ pass class StoreBuy(Handler): def work(self, robot, response_data): state = response_data.get('') commands.store_buy(serverId, client, packId, giftId, useTicket, ticketId) class StoreInfo(Handler): pass
2.4 消息处理层
这部分已经是游戏客户端的逻辑了。
2.4.1 客户端数据将客户端定义为 Robot 类,客户端与服务端的交互数据都放在这个类中。
import multiprocessing class Robot(object): inputs = list() outputs = list() socks = dict() playerData = dict() packId = 1 def __init__(self, client): self.client = client self.queue = multiprocessing.Queue() 2.4.2 消息监听
使用 select 模块,由于是系统暴露的接口,效率比较高。我的破电脑是 windows 的,没办法用 epoll。在做压力测试的时候,大量的游戏客户端跑在一个机器上,用 epoll 效率就高很多了。
import select class Robot(): def message_collector(self) while self.inputs: print(self.inputs) readable, _, _ = select.select(self.inputs, [], []) for sock in readable: response = self.client.receive() self.queue.append(response) 2.4.3 消息处理
Handler 类中,取所有接口处理器字典作为 handlers。
handlers = { "store@buy": StoreBuy, "store@info": StoreInfo }
Robot 中添加接口处理机制:
class Robot(object): @staticmethod def handler_selector(cmd): return handlers.get(cmd) def message_handler(self): while True: response = self.queue.get() handler = self.handler_selector(response[0]) handler.work()
3.如何实现接口自动化
其实当你完成了游戏的客户端,那么测试的设计,就是小菜一碟了,理论上就是运行过程中的数据验证问题。
3.1 设计思路
3.2 测试用例
3.2.1 代码部分在 Handler 中添加 test 方法,正常逻辑是走 Handler 的 work 方法,测试验证逻辑走 test 方法。
class Handler(object): def __init__(self, robot): pass def work(self, robot, response_data): """ 事务用处理器 """ pass def test(self, robot, response_data, expect): """ 测试用处理器 """ pass class StoreBuy(Handler): def test(self, robot, response_data, expect): state = response_data.get('state') if state == expect['state']: print("test pass") else: print("test failed") 3.2.2 数据部分
测试用例主要有 4 个部分:id,接口名,发送的数据,期望返回数据。
testCases = [ { "id": 163, "command": "store@buy" "data": {"giftId": 126, "useTicket": 1, "ticketId": 43992687} "expect": {"state": 200} }, { "id": 164, "command": "store@buy" "data": {"giftId": 126, "useTicket": 1, "ticketId": 0} "expect": {"state": 402} } ]
3.3 测试过程
3.3.1 执行逻辑我们的验证逻辑是写在处理器的 test 方法里的,因此,只需要在 message_handler 中判断是不是需要调用 test 方法。
class Robot(object): def message_handler(self, testcase): while True: response = self.queue.get() handler = self.handler_selector(response[0]) if response[0] == testcase.get('command'): handler.test() else: handler.work() 3.3.2 结果记录
Robot 类添加一个结果收集列表。
class Robot(object): testOutput = list()
将执行结果输出到列表中
testOutput = [ { "command": "store@buy", "state": "fail", "playerData": self.playerData } ] 3.3.3 日志输出
为 Robot 对象添加 logging 方法,将日志输入到文件中
import time class Robot(object): def logging(self, level, msg): with open("log.txt", "a") as fp: data="{datetime} [{level}] {msg}".format( datetime=time.strftime("%Y/%m/%d %H:%M:%S"), level=level, msg=msg) fp.write(data+"n")
3.4 测试报告
大家自行选择合适的展示方式,我倾向于将结果输出到 html 文件中。
3.5 持续集成
使用 linux 自带的 crontab 模块,定时执行测试任务,生成测试报告。
利用 Jenkins,新建触发器 Poll SCM,新建自动化接口测试任务,代码更新后触发自动化测试。
3.6 小结
这种验证方式,理论上适用所有基于 socket 协议的 C/S 应用,大家可以自行尝试。
4.拓展应用
4.1 游戏测试场景构建
假如游戏使用了 H2 数据库,这是一种内存数据库。这意味着游戏运行时,你很难直接修改数据库的数据,除非开发人员事先开发了专用的接口。某一个活动需要 2000 人报名,并且每个人都需要通过战斗获取勋章和积分,最后根据勋章和积分排名每个人都有随机的奖励。
对于这样一个测试场景,传统造数据方式:先停服,然后编写 SQL 将数据写入 MySQL,然后启动游戏服,让数据从 MySQL 数据库中读入内存数据库。
如果时使用接口自动化的工具,那么只要开启多进程,发送活动报名接口,再发一些战斗接口,就很轻松地创建了测试场景。既方便了测试,方便了前端开发。
4.2 压力测试
选择单一接口或场景,多进程跑接口,对服务端施加负载。
jvm 运行:使用 jstack,Jprofiler 等工具,
接口响应时间:游戏客户端编写接口响应时间统计模块(思路:接口发送时间和接收时间差,由于服务端存在推送包,数据包的发送后收到的包不一定是响应包)。
cpu、内存占用,psutil 和 matplotlib 绘制性能曲线。
TPS:awk 统计服务端日志
4.3 机器人仿真模拟
给游戏客户端制定一定的行为策略,就是游戏机器人。机器人可以模拟用户的操作,我们可以收集信息,供策划决策。
策划希望研究不同计策对游戏胜负的影响,那么我只要设计场景,游戏机器人带不同计策,记录游戏结果。重复若干场,得到统计数据。策划可以分析数据,调整计策的数值。
比如火攻计策对玩家的伤害过高,不符合策划对于改计策的定位,也影响了玩家的游戏体验,削弱。
我们知道王者荣耀的英雄数值,即使上线后还在调整,如果有了机器人仿真模拟,得到了某英雄太弱,可以提前调整数据,从而避免了把问题带到了线上。
5.作者的话
我一个测试搞了这么多幺蛾子干嘛,好好做功能测试不好吗
写这篇文章花了 4 天时间,不知道版主大哥能不能赏脸给个精华贴。
相关知识
詹聪聪:游戏接口测试自动化实践 · 测试之家
烟雨江湖蓝鲸屿聪聪位置在哪 蓝鲸屿聪聪位置介绍
《烟雨江湖》聪聪位置
烟雨江湖聪聪隐藏试卷答案是什么 烟雨江湖聪聪隐藏试卷答案一览
星穹铁道npc小聪在哪
你是大聪明空巢老人怎么通关
移动端游戏测试一些问答 · 测试之家
游戏测试之浅谈测试思维
如何成为一名游戏测试(QA)
游戏开发测试和质量保证最佳实践
推荐资讯
- 1老六爱找茬美女的烦恼怎么过- 4999
- 2博德之门3黄金雏龙法杖怎么得 4867
- 3《大侠立志传》剿灭摸金门任务 4312
- 4代号破晓官方正版角色介绍 4023
- 5赛马娘锻炼到底的伙伴支援卡事 3802
- 6闪烁之光11月兑换码大全20 3774
- 7原神原海异种刷怪路线-原神原 3547
- 8爆梗找茬王厕所特工怎么通关- 3542
- 9《我的世界》领地删除指令是什 3440
- 10原神开局星落湖怎么出去 原神 3426