游戏测试专题,从零开始的自动化测试框架
在传统游戏测试中,主要分为两种方向的测试。一种是功能测试,即通过手工跑游戏的方式来验证功能的正确性。而另外一种是自动化测试,也就是测试开发,可以通过跟代码相结合的方式保证产品质量和测试效率
在传统游戏测试中,主要分为两种方向的测试。一种是功能测试,即通过手工跑游戏的方式来验证功能的正确性。而另外一种是自动化测试,也就是测试开发,可以通过跟代码相结合的方式保证产品质量和测试效率,比如做一些测试工具等。
自动化测试的优势在于,可以使用代码辅助,做一些重复性较高的测试工作,例如回归玩法逻辑,或者一些手工测试较难达成的测试,例如多人战斗等,同时由于是全自动的,也不受时间限制,只要有机器资源就可以运行。
游戏都是在不断扩充玩法内容的,因此在游戏功能越来越多的情况下,单单依靠手工回归玩法是非常耗时间,且性价比不高。
在自动化测试中,比较重要的一个方向是测试脚本。脚本中可以书写测试逻辑和检查点,让机器代替手工完成这些测试。例如日常任务,在没有迭代的时候,往往是用非常固定的模式就可以完成的,第一步去做什么事情(比如采集某个物品),第二步去打什么怪,以此类推,最后完成任务时检查一下投放的奖励是不是对的。这类重复性非常高的测试工作,交由机器去做,一方面可以节约手工测试的时间,另一方面可以提高整个测试的时效性,更早地发现问题,例如在夜深人静的时候就跑完了测试,早上上班就可以看到结果。
综上,自动化测试在游戏产品的测试中是非常重要及必须的,当项目组拥有一个良好的自动化测试体系后,对整体测试效率和质量都是非常大的帮助。
注:自动化测试所涵盖的方向非常多,本文中所述的自动化测试框架,主要就是指测试脚本进行测试时所使用的框架。
一、自动化测试框架
想要驱动测试脚本进行测试,必然需要有一套体系来完成。例如在《逆水寒》中,我们有一套完备的自动化测试框架。
对于网游来说,存在服务器和客户端,脚本是不限定跑在服务器或者客户端上的,这可以按需指定。
但是为了更好的模拟玩家的操作,这里还是推荐大多数脚本都是客户端上驱动的,这样不仅可以覆盖服务器的流程逻辑,还可以覆盖客户端的逻辑,例如UI坏了这类问题。
一套好的自动化测试框架,往往做了很多抽象和封装,将底层的驱动和上层的逻辑尽可能的分开。这样在书写脚本逻辑时,无需非常深入的了解底层架构,降低了书写脚本的难度门槛,甚至一些功能测试也能进行简单的测试用例的脚本实现。这里的脚本其实就是想要实现的逻辑,例如操作主角去跟什么NPC对话,再打几个怪之类。而框架本身的内容,例如如何驱动和流程,交由自动化QA来管理。
由于游戏产品的服务器和客户端等架构可能会有一些不一样,以及脚本需要借助游戏本身的代码逻辑,因此很难找到一套非常通用的,直接就能拿来用的框架。
但是依然可以抽象出一些通用的功能模块,只需要各个项目组实现相应功能的模块,就可以将整个流程串起来。
二、自动化测试流程
1、流程概述
这是《逆水寒》中使用的自动化测试的流程:
①从每日打包开始,有脚本一直在监控打包的情况,一旦发现一个新的包打完了,那么就会自动开始整个流程,即监听打包。
②我们游戏也是分服务器和客户端的,首先会驱动脚本下载服务器并将测试用的代码塞到服务器路径下,以方便服务器加载这部分代码(这里是为了避免自动化的代码影响到外网,所以按需加载)
③启动服务器
④下载客户端部分,启动客户端
⑤执行自动化脚本
⑥直到所有的测试脚本都完成
⑦将最后的结果汇总和分析,并发送测试报告
2、模块拆分
从上述流程中可以看出,要想实现全流程自动化,需要以下几个大的模块:
①打包监听。即自动开启后续新的自动化测试流程。
②服务器流程脚本。下载服务器,部署服务器,按需修改配置文件,按需塞文件,启动服务器。脚本跑在服务器机器上。
③客户端流程脚本。下载客户端,按需修改配置文件,按需塞文件,启动客户端,开始跑测试脚本,等跑完脚本,上传日志。脚本跑在客户端机器上。
④结果分析与处理。分析日志,生成报告。不限定跑的机器位置,因为日志可以现场拉取。
⑤测试脚本驱动和管理。驱动整个测试按照测试脚本进行下去,并且管理所有的测试脚本。
⑥测试脚本逻辑。具体的玩法脚本逻辑,例如玩法流程、步骤以及检查点。
①-④的过程是完全串行的,无论使用什么实现手段,只需要上述功能能够串行起来即可。⑤⑥是在执行自动化脚本时所需要的模块,也是整个自动化流程中最难的一部分,后面会详细讲解。
三、执行自动化脚本
1、概述
执行自动化脚本主要是两大块内容,一个是底层框架,即测试脚本驱动和管理,另一个是具体玩法的测试脚本逻辑。
在逆水寒中,前者在服务器和客户端均有一定的代码,后者是运行在客户端上的,是针对某一个功能的详细的测试逻辑和流程。
2、背景知识——状态机
在叙述框架之前,先了解一下所使用的基本概念——状态机。
通常我们所说的状态机都是指有限状态机。
有限状态机是一种用来进行对象行为建模的工具,其作用主要是描述对象在它的生命周期内所经历的状态序列,以及如何响应来自外界的各种事件。在计算机科学中,有限状态机被广泛用于建模应用行为、硬件电路系统设计、软件工程,编译器、网络协议、和计算与语言的研究。
状态机是一个抽象的数学模型,状态机其实在平时使用的非常广泛,例如if-else语句,也可以理解成一个简单的状态机,当if条件满足时,就走到分支一这个状态;当if条件不满足时,就走到分支二这个状态。
举个很简单的例子,开关灯。
灯有两个状态,黑灯和亮灯,黑灯状态下,如果有人去开灯,那么状态就会切换为亮灯。而亮灯状态下如果有人关灯,那么状态也会切换到黑灯状态。
状态机的全称是有限状态自动机,自动两个字也是包含重要含义的。给定一个状态机,同时给定它的当前状态以及输入,那么输出状态时可以明确的运算出来的。例如对于开关灯,给定初始状态黑灯,给定输入“开灯”,那么下一个状态时可以运算出来的。
状态机有四大概念:
第一个是 State,状态。一个状态机至少要包含两个状态。例如上面的例子,有亮灯状态和黑灯状态两个状态。
第二个是 Event,事件。事件就是执行某个操作的触发条件或者口令。对于上面的例子,“开灯”就是一个事件。
第三个是 Action,动作。事件发生以后要执行动作。例如事件是“按开灯按钮”,动作是“开灯”。编程的时候,一个 Action 一般就对应一个函数。
第四个是 Transition,变换。也就是从一个状态变化为另一个状态。例如“开灯过程”就是一个变换。
自动化测试的脚本也是利用了状态机模型的思想,脚本的执行即在不同的状态中流转,以实现目标的流程。
3、底层框架
框架主要包含下面几个功能:
- 驱动测试脚本执行(客户端)
- 管理测试状态(服务器)
- 维护一些公共对象库和函数库
- 记录测试日志
由于脚本逻辑是跑在客户端上面的,因此驱动测试脚本的进行一般也是客户端来执行。
而管理测试状态,因为有可能连接多个客户端,那么使用服务器进行管理是比较合适的方式。同时,一般为了安全起见,客户端不会暴露给外部脚本通信的可能性(比如有外挂风险),而服务器一般可以有通信方式连接上去,这样通信相对方便。
这里是整个自动化框架的一个大的概念图。
最右边客户端部分,主要是一个tick来切换状态,最左边是服务器,我们的游戏里master是个总控的线程,所以用它来管理测试的进行,对于不同的游戏也可以选取不同的服务器进程。具体的原理和逻辑下面会细讲。
中间是服务器和客户端之间的通信,服务器是一个管理者的角色,它主动发起测试,然后客户端是个执行者,执行结束后,将结果返回给服务器。那么这里就有两次通信。我们游戏中使用的是rpc通信,直接复用了游戏中客户端和服务器之间的通信模式。当然这个根据不同的游戏和其自身的通信架构,也可以有不同的的实现方式,只要能保证两边互通即可。
(1)驱动脚本执行
- 始终执行的tick(例如1s一次)
- 维护的状态列表(状态栈/队列或者其他结构)
- 适当的日志打印
我们拆开一点点的说,举点例子可能更好理解。
首先,客户端这里是要驱动脚本的执行的,客户端存在一个贯穿始终的tick,一般来说游戏都存在tick,就是定时器。在测试的时候,我们肯定不可能一次性把逻辑跑完。
游戏存在客户端和服务器,两者需要通信,并且服务器是多进程的,有进程间通信,全部这些都是异步的,客户端执行的太快容易造成逻辑顺序问题,例如服务器还没返回给结果,因此客户端的逻辑在执行过程中需要等服务器执行,等各种逻辑往下走。
然后,客户端的逻辑是单线程的,使用tick可以避免把客户端一直执行自动化的逻辑卡死在自己的逻辑里。用tick推动测试脚本,测试脚本需要执行Action,Action会推动游戏逻辑的执行,而用tick,就是可以去监控在执行Action过程中,State是否改变了。
再者,这种快速跑完逻辑也不符合玩家的操作,正常玩家也不可能在0.1s内点击几万次按钮,我们的测试脚本还是期望能模拟玩家的操作。
所以这里我们依赖定时器,例如1s一次的定时器。1s只干一件事情,这样就可以让我们的操作变慢,变得跟玩家的习惯一致。
除了定时器,还要维护一个状态序列。
状态列表
此处状态列表的目的主要是用于管理状态流转,使用队列或者栈等数据结构均可。
状态列表存储着所有将要执行的状态以及他们的顺序,从而在tick来临的时候,自动化可以知道下一个要执行的状态函数是什么;除了能够拿到调用函数外,这里的状态列表也可以进行插入和删除等操作,比如要插入什么新的待执行的状态,或者当前状态执行完了就可以从里面删除掉了。
也就是说状态列表有如下的功能:
①取要执行的状态函数
②插入新状态
③删除旧状态
④管理所有状态的顺序
逆水寒中使用的是状态栈,状态栈中放着状态名和它使用的方法。
每种数据结构都有其利弊。
使用栈的方式,后进先出,方便在执行过程中插入状态,但是整体顺序需要理清楚,可能有点绕,因为后面插入的反而会先执行,在批量加状态时,是个倒序,理解起来成本比较大。
使用队列的方式,先进先出,方便批量插入状态时的顺序理解,但是插入马上要执行的状态时比较麻烦。
下面会以状态栈的方式来具体讲述一下状态数据。
以流程图所示为例,客户端注册了一个tick,tick里面就是去取状态栈栈顶的状态和它的函数,执行这个函数,然后进行一些状态转换,比如这个状态执行完了,可以出栈,或者说还要重复执行这个状态,那么就不变,以及当前这个状态执行完、出栈了,下面还需要接一个新的状态,那么可以把新的状态再压栈。状态转换后,又开始等下一次tick,那么下一次tick取到的栈顶函数,就是已经更新后的了。
这样的循环就可以使得整个脚本按照既定的状态顺序执行下去。
举个例子,这里脚本先给状态栈里压了两个状态,栈顶是点按钮,下面是寻路。
Tick第一次取栈顶函数,是个点按钮的函数,点击后,把这个状态出栈了,那么状态栈就只剩下一个寻路状态,下次tick取栈顶函数,取到的就是寻路。
这就是最简单的状态转换。
这种状态转换就可以实现做了动作1然后做动作2、动作3以此类推的逻辑了。
对于寻路这种,它是个持续性的动作,这种对栈的操作就比较复杂。
比如开始寻路后,如果判断到达目的地了,那么就认为寻路完成了,这个状态可以出栈了,后面再按需看是否要加什么新状态,或者之前已经加过的序列继续执行下去。
如果没有到达目的地,有个变量计数,记录当前跑过多少次了,如果已经超过了最大超时次数,那么就认为寻路失败了,因为这里不可能无限制的等下去。寻路状态出栈,失败处理状态入栈。这里的失败处理主要是停掉这个测试用例,并将失败的结果返回给服务器。如果还没超时,那么就继续等待寻路,状态栈不变,重复执行。
公共对象库&函数库
像寻路这类状态,在自动化测试中的使用非常广泛,是非常常用的。因此可以将其封装为常用状态,只需要给目的地参数即可驱动。
那么在写脚本时,就无需每个脚本都实现一遍这个状态,而是直接从公共库中拿出来用。
这样的好处不仅仅是在下一次使用时较为方便,另一方面在产品有改动造成的状态改动时,能够尽可能减少修改的工作量,不然可能要每个脚本都改一遍。
再比如一些常用的界面操作,比如开背包,关背包等都可以封装为常用状态或者公共函数便于使用。
这些都是在写脚本过程中不断积累的,在搭好框架后,不断补充脚本的过程中,可以考虑下当前需要的状态是不是之后可能会复用的,如果是,可以提出来作为公共对象库和函数库。
管理测试状态
- 监控测试进度
- 调度分配测试用例
为了最大化利用测试时间和硬件资源,通常会有多台客户端并行跑脚本。也就是一个服务器带着多个客户端来进行自动化测试,最终将测试结果合并整理。
这里用到了分布式系统的思想。分布式系统是由一组通过网络进行通信、为了完成共同的任务而协调工作的计算机节点组成的系统,也就是master-slave模式。
那么如果我们有6台机器,脚本需要跑36小时,对于6台客户端并行来跑,测试时间就会降低到大约6小时,大大提高了测试的时效性。
在自动化测试中,服务器充当了master的角色,用来调度和监控,而所有的客户端都是slave,听从服务器的命令,跑相应的脚本。
服务器会知晓有几个客户端,以及每个客户端在跑的脚本情况。一旦有一个客户端是空闲的,就可以将没有跑过的脚本分配给它,使它工作起来。当一个客户端跑完某个脚本后,告知服务器已经跑完了和最终的执行结果,服务器会感知到这个客户端的上一份工作已经结束了,那么它目前的状态就是空闲,可以发新的工作给它了。这个循环会一直持续到所有需要跑的脚本都跑完为止。
对于服务器来说,它的主要作用是管理测试状态,包括监控测试进度和调度分配测试用例。
分发测试用例后,让相应的客户端开始执行测试,服务器就会等待客户端返回执行的结果。
如果测试通过,那么服务器会记录这个测试用例已经完成了,是通过的;如果测试失败或者超时了,那么服务器也会记录这个用例结束了,但是结果是失败。
在收到客户端无论通过与否的结果,就会停止当前的用例,继续分发给客户端新的用例。
这个流程直到所有的测试用例都完成了。
记录测试日志信息
- 记录测试时的状态信息用于调试和查错
- 脚本失败/有异常时记录日志
- 脚本正常时仍需要一定日志用于后期查错
对于客户端来说,它还需要适时的记录一些日志,方便调试和查错。无论脚本是否正常,都需要记录合适的日志。如果脚本失败了,或者有异常,像图中记录了一句err,不仅可以作为后续测试报告的关键词提取,也可以作为标注这里发生了什么问题。
脚本正常的时候,也可能需要打印一定适量的日志,比如这里寻路,我能知道寻路点了下连接,然后开始等,当前玩家的坐标是多少,距离目的地多远,这些信息都可以用于后期查错。
总结
那么底层框架到底需要哪些模块和功能呢?
服务器 | 客户端 |
通知客户端开始测试 | 维护状态栈(队列) |
给客户端发送测试用例(脚本)的能力 | 状态入栈/出栈(队列) |
维护当前完成的/进行中的/未完成的测试用例 | tick驱动,执行栈顶方法(队列头) |
等待客户端用例的执行结果 | 给服务器发送测试用例执行结果 |
客户端完成用例后继续调度未完成的用例 | 记录日志 |
四、测试脚本
1、概述
测试脚本主要用于书写具体的玩法逻辑,以模拟玩家操作为主。
这里通常的实现手段都是通过界面点击等各类操作进行的,毕竟这样的操作方式更贴近玩家。
测试脚本使用了状态机的思想,事件就是一个个处理函数,通过执行一个个事件,来驱动脚本的状态执行下去。比如接任务-跟NPC对话-杀10个黑衣人-提交任务,都是按照既定流程执行下去。
2、界面操作
针对界面操作,也有不同的方式。以下两种用的比较多:
①、根据控件找坐标,模拟鼠标和键盘事件。
②、根据图像识别位置,模拟鼠标和键盘事件。
逆水寒目前使用的是第一种方式,根据控件的路径,可以在UI层获取到这个控件对象,并计算一个合适的x,y平面坐标,即鼠标点击的位置。这里引擎层提供了一些底层点击、拖拽等界面操作方法,接受的函数一般都是x,y坐标。那么通过这两者的结合,就可以使需要的界面操作执行起来。
3、检查点
除了界面操作外,我们还会有一些检查点,比如检查奖励是否正确。
有些可以通过获取UI上显示的值进行,还有一些可以利用客户端本身的代码逻辑,去客户端里取数值。
4、服务器指令
在测试过程中,可能会需要构造一些环境,比如角色要拥有多少钱,需要多少级,这类通常在手工测试时采用gm指令的方式发送给服务器执行。
对于脚本来说,它也会有类似需求。可能在走到某一步的时候,需要服务器执行指令。
在《逆水寒》中,新加了自动化专用的rpc,可以从客户端发送到服务器指令。当然这里能从客户端给服务器发指令是有安全风险的,因此这里一方面只使用服务器加载过的这些名字的指令(即提前封装了指令),另一方面,只有在内网开启自动化的情况下,服务器才会加载指令。(简言之,外网的服务器是没有任何可以执行的自动化指令的,因为它什么都没加载。)
点击下面卡片关注本博主个人公众号可以获取更多技术干货,包括自动化测试学习资料,知识体系大纲,40篇面试经验文章和项目案例源码、笔记等等
可拉进交流群,有大佬指点迷津你的问题往往有人遇到过。具体详细内容可自行查看哦!
▲ 《程序员一凡》 ▲
关注上方公众号获取
开放原子开发者工作坊旨在鼓励更多人参与开源活动,与志同道合的开发者们相互交流开发经验、分享开发心得、获取前沿技术趋势。工作坊有多种形式的开发者活动,如meetup、训练营等,主打技术交流,干货满满,真诚地邀请各位开发者共同参与!
更多推荐
所有评论(0)