游戏测试之接口工具篇(下篇)

上一篇分享了在做接口测试时,用到的一些工具软件,最终的感觉是,能用,但不顺手。最主要的原因是,发送请求后,没法很直观地查看服务端的返回结果,其次是使用起来,工作效率并不理想。

在经历的几个项目中,最终都是使用了内部开发的接口工具,而每个项目的接口工具的原理和使用方式区别还挺大,在此分享下各个工具的设计思路。

项目1

2011年那会,刚刚从学校踏入测试坑,项目是一个SLG页游,前后端使用Socket通讯,数据交互采用Json格式,最初版本的接口测试是用WPE做的,后来服务端的主程MM在内网开了一个Web端口,可以接收Http Get请求,格式如下:

1
http://192.168.22.248/sftx/gameSocket/send?u=playerId&c=protocol&p=port&params=param1|param2|param3

参数的含义如下:

1
2
3
4
u:玩家ID
c:操作协议号
p:服务器端口号
params:参数列表,多个参数使用"|"连接

举个栗子:玩家升级主城

1
2
3
4
玩家ID:7001
操作是升级主城:协议号110001
参数列表:主城建筑ID是1001
内网端口号:默认5001

所以相应的Get请求是

1
http://192.168.22.248/sftx/gameSocket/send?u=7001&c=110001&p=5001&params=1001

把这个Get请求发送过去后,Web服务器会解析出相应的玩家ID,操作协议,参数列表,自动开启一个Socket连入内网游戏服务器,执行相应的操作,并返回处理结果。

从此脱离了WPE,当时是用PHP围绕这个Web接口,做了一个调用页面,也实现了请求的新建,修改,分类保存,批量发送,并解析返回结果。

基于这个接口,也实现了一些自动化的批量操作,如批量建号,批量升级等,对工作效率的提升也是很明显的,比如新建军团后,跑个批量申请入团,军团就满人了。

这个模式的不足之处:发送请求后,因为Web那边会自行创建新的Socket连接,会自动挤号。

项目2

项目2是一个RPG页游,前后端使用Socket通讯,数据交互格式是AMF3。

当时后端底层在重写ing,所以没空折腾一个Web端口给我调用~这个时候前端主程FF站了出来,提出了一个方案。

游戏在网页上加载的时候,同时也加载一个测试用的js文件。

执行接口测试的方式,是在Chrome的console窗口,输入已经加载的js函数sendCommand,把操作内容作为函数的参数,回车运行后发送给Flash客户端,Flash客户端接收后,解析出相应的操作ID和参数列表,执行后在console窗口打印出服务端返回结果。

函数调用格式如下:

1
2
3
4
sendCommand(“PackagesController”, “move”, 0, [“BACKPACK”,0,“BACKPACK”,30]);
操作模块:PackagesController 背包模块
操作行为:move 移动背包物品
参数列表:[“BACKPACK”,0,“BACKPACK”,30] 从第0个格子移动到第30

这个模式的一个好处:接口测试的请求是前端解析后,自行发出的,所以不会被挤号。

项目3

项目3是一个回合制的RPG手游,客户端使用Unity,Socket通讯,数据交互格式是PB,作为工作室的第一款手游,这次前后端主程都一直很忙,接口工具在项目前期一直没有落实下来。中间也一度是用WPE和烧饼顶着先大概测下接口。

随着两个项目以来的一些积累,这次开始自己尝试独立完成接口工具,工具需求规划如下:

  • 支持录制请求
  • 可以对录制的请求进行复制,删除
  • 解析请求里边的参数列表
  • 查看服务端的返回结果
  • 自动校验返回结果
  • 测试用例保存到本地

实现过程中的一些积累在此记录下:

录制的原理

点击一个技能升级的按钮的背后发生了什么?

  1. UI接收到点击请求,调用技能模块
  2. 技能模块准备好参数列表,调用Server层的Send方法,生成一个请求
  3. Server层接收后,对请求进行封装,加入校验key和请求头,压缩为PB格式,生成最终请求
  4. 发送给服务端

那么,要从哪里切入来录制请求呢?最终选择了在Server层接收后,对请求进行封装前,主要原因是,接口测试主要关注参数的不合理修改后,服务端能否做出正确判断,可以不用关心校验Key等其他信息,对请求进行修改后,点击发送,直接调用Send方法,底层就会完成新的请求封装和发送。省代码啊~

于是乎,现在的问题是,录制的代码加在哪里?

这个项目采用的方式,是直接在Send函数里边,嵌入了转存请求的代码。

1
2
3
4
5
6
7
8
public int Send(int controllerID, int handlerID, IEnumerable<object> paramList, Action<GodResponse> callback) {
// 煎饼加入
TestCenter.AddRequest(new TRequest(controllerID, handlerID, paramList, callback));
// 煎饼加入
var request = CreateRequest(controllerID, handlerID, paramList, callback);
SendRequest(request);
return request.ID;
}

TRequest的定义

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
[Serializable]
public class TRequest :ICloneable {
int controllerID;
int handlerID;
IEnumerable<object> paramList;
Action<GodResponse> callback;
public TRequest(int controllerID, int handlerID, IEnumerable<object> paramList, Action<GodResponse> callback) {
this.controllerID = controllerID;
this.handlerID = handlerID;
this.paramList = paramList;
this.callback = callback;
}
public object Clone() {
MemoryStream stream = new MemoryStream();
BinaryFormatter formatter = new BinaryFormatter();
formatter.Serialize(stream, this);
stream.Position = 0;
return formatter.Deserialize(stream);
}
}

这个做法其实不合理,因为我们项目的测试代码并不提交到项目的代码库,每次更新代码,都会出现冲突,更合理的方式是前端提供一个发送请求的事件,测试代码这边进行监听(下个项目已使用这种方式)。

测试工具UI

UI界面选择了Unity古老的OnGUI方法,原因就是:易学,够用。下边是一个简单的GUI界面。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
using UnityEngine;
public class TestUI : MonoBehaviour {
private Rect windowRect = new Rect(Screen.width * 0.25f, 0, Screen.width / 2, Screen.height - 10);
public Vector2 scrollPosition = Vector2.zero;
void OnGUI() {
windowRect = GUI.Window(0, windowRect, WindowFunction, "接口测试工具");
}
void WindowFunction(int windowID) {
GUI.DragWindow(new Rect(0, 0, Screen.width/2, 30));
GUI.Box(new Rect(0,0,Screen.width,Screen.height),"");
GUILayout.BeginArea(new Rect(5, 20, Screen.width / 2-20, Screen.height));
scrollPosition = GUILayout.BeginScrollView(scrollPosition,GUILayout.Width(Screen.width / 2 - 20),GUILayout.Height(Screen.height-60));
// 在这里请求列表解析
GUILayout.EndScrollView();
GUILayout.BeginHorizontal();
if (GUILayout.Button("统计数量")) {
}
if (GUILayout.Button("清空记录")) {
}
if (GUILayout.Button("录制")) {
}
if (GUILayout.Button("停止")) {
}
GUILayout.EndHorizontal();
GUILayout.EndArea();
}
}

效果图:

参数列表解析

参数列表是一个object类型的数组,所以里边可以放各种基础类型,解析的时候,需要用到反射,动态修改里边的内容,解析函数如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
public void ParseBaseType2(object aParam, object aList, int index = 0) {
GUILayout.BeginHorizontal();
Type paramType = aParam.GetType();
if (paramType == typeof(string)) {
GUILayout.Label("String", GUILayout.Width(30));
aParam = GUILayout.TextField(aParam.ToString());
} else if (paramType == typeof(short)) {
GUILayout.Label("Short", GUILayout.Width(30));
aParam = Convert.ToInt16(GUILayout.TextField(aParam.ToString()));
} else if (paramType == typeof(int)) {
GUILayout.Label("Int", GUILayout.Width(30));
aParam = Convert.ToInt32(GUILayout.TextField(aParam.ToString()));
} else if (paramType == typeof(long)) {
GUILayout.Label("Long", GUILayout.Width(30));
aParam = Convert.ToInt64(GUILayout.TextField(aParam.ToString()));
} else if (paramType == typeof(bool)) {
GUILayout.Label("Bool", GUILayout.Width(30));
aParam = Convert.ToBoolean(GUILayout.TextField(aParam.ToString()));
} else {
GUILayout.Label("type can not parse,type is " + paramType.Name);
GUILayout.EndHorizontal();
return;
}
var removeAtMethod = aList.GetType().GetMethod("RemoveAt");
removeAtMethod.Invoke(aList, new object[] { index });
var insertMethod = aList.GetType().GetMethod("Insert");
insertMethod.Invoke(aList, new object[] { index, aParam });
GUILayout.EndHorizontal();
}

最终成品图:

项目4

SLG手游,依旧Socket通讯,依旧PB。

在上一个接口工具的基础上,新增了:

  • 更详细的用例设计和用例库的读取和保存(使用C#的序列化)

  • 增加自动化接口测试模块,测试结果通过Http请求发送到Web服务器

  • 增加一个Web服务器接收测试结果并进行统计(使用Flask搭建),测试结果用图表形式展示(Echarts)

Coding,Coding~