3

GS 测试规范实践

 3 years ago
source link: https://wudaijun.com/2020/08/gs-testing-practice/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.

GS 测试规范实践

2020-08-15gameserver

41 0

在之前的博客中几次简单提及过给GS做测试,关于测试的必要性不用再多说,但在实际实践过程中,却往往会因为如下原因导致想要推进测试规范困难重重:

-. Q1: 写测试代码困难: 代码耦合重,各种相互依赖,全局依赖,导致写测试代码”牵一发而动全身”,举步维艰
-. Q2: 测试时效性低: 需求变更快,数值变更频繁,可能导致今天写好的测试代码,明天就”过时”了
-. Q3: 开发进度紧: 不想浪费过多时间来写测试代码,直接开发感觉开发效率更高

要想推进测试规范,上面的三个问题是必须解决的。这里简单聊聊我们在Golang游戏后端中的测试实践和解决方案。我们在GS中尝试的测试方案主要分为四种: 单元测试,集成测试,压力测试,以及模拟测试。

单元测的优点是与业务逻辑和外部环境关联度最小,同时go test也很容易集成到CI/CD流程中。单元测试的缺点就是上面提到的Q1(耦合依赖问题),对此,我们的解决方案是:

  1. 持续重构,解耦降低依赖。有点废话,但是写易于测试的代码确实是一种修行
  2. 通过goconvey测试框架简化单元测试的编写
  3. 通过gomock Mock掉接口依赖
  4. 实在Mock不掉的,通过gomonkey Hack掉依赖,不过要记得禁用内联
  5. 对于一些复杂的单元测试,如涉及到发消息,创建玩家,启动定时器等,可以创建通用的Mock组件和环境,便于使用

goconvey+gomonkey+gomock 三件套在实践中足够灵活强大,具体使用参考文档即可,比较简单,就不展示了。

集成测试我们又称之为用例测试,它是一种黑盒测试,以C/S交互协议为边界,站在客户端视角来测试服务器运行结果,黑盒测试本质上是消息流测试。它的优点是覆盖面广,网络层,集群管理,消息路由等细节都被会覆盖到。黑盒测试的难点在于易变性,协议变更,配置更新等都可能造成测试用例不可用,即上面提到的Q2(用例时效性问题)。对此,我们的实践是:

  1. 将消息流测试离线化,即封装基本原语(Send,Wait,Expect,Select等),化编译型为解释型,让测试用例可以通过类似配置文件的方式来描述,简化与服务器的交互细节,甚至理论做到交付给非技术人员使用。技术上除了对模拟客户端的封装外,主要是对json的处理: gojsonq, jsondiff, jsonx
  2. 写可重入的测试用例,可重入即用例不应该依赖于当前服务器和用例机器人的初始状态,做到可重复执行
  3. 保存一份专用于用例测试策划配置快照,避免频繁的数值调整导致测试用例不可用。服务器和测试客户端都使用这份配置。即GS需要支持不同的配置源(如DB/File)

以下是一个省掉很多细节的测试用例(yml格式描述):

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
# 封装一个Function,从预定义变量varRole[n]中提取字段放到自定义变量中
InitAttackCmds:
- find LoginAck.city.coord.X from varRole1 to varCity1X
- find LoginAck.city.coord.Z from varRole1 to varCity1Z
- find LoginAck.city.cityID from varRole1 to varCity1ID

# 单个测试用例
AttackPersonCityWinTest:
# 创建两个Robot,以Rbt1 Rbt2 标识
- newrobot 2
# 此时机器人已经登录完成,初始化自定义变量
- call InitAttackCmds
# 获取Rbt1初始化城防值
- Rbt1 send CityDefenseReq {}
# wait 后面的消息支持json局部字段比较(包含匹配)
- Rbt1 wait CityDefenseAck {isCombustion:false}
- Rbt1 find cityDefense from varLastAck to varCityDefensePreVal
# Rbt2 向 Rbt1 城池行军
- Rbt2 send NewTroopReq {"Action":1,"Soldiers":{"11211001":500,"11211301":500},"EndCoord":{"X":%v,"Z":%v},"Mission":{"IsCampAfterHunt":false,"IsRally":false},"TargetID":%v} varCity1X varCity1Z varCity1ID
- Rbt2 wait NewTroopAck {errCode:0,action:1}
# 防守失败后被烧城
- Rbt1 wait CombustionStateNtf {isCombustion:true}
- Rbt1 find cityDefense from varLastAck to varCityDefensePostVal
# 掉城防值
- should varCityDefensePostVal < varCityDefensePreVal

压力测试也是黑盒测试的一种,它的目标是放大服务器的性能问题以及并发状态下的正确性问题。我在如何给GS做压测中简单地阐述过压测的一些注意事项。简单来说,用例测试注重特例和自动化,而压力测试注重随机和覆盖率。

模拟测试是指通过类似console的方式来模拟客户端,它的功能主要分为两部分:

  1. 动态构造消息并返回响应数据
  2. 支持一些简单的GM,如查看/修改自身数据

它最大的优点在于灵活性,主要有两个作用:

  1. 服务器新功能开发完成进行快速自测验证(脱离客户端),提升开发效率
  2. 出现某些疑似服务器的BUG时,登录已有角色进行数据验证和Debug

以下是我们的模拟测试的样子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 注: FC[...]# 为输入行,其余为输出行    "//..."表示省略消息具体内容
FakeClient connect successed
FC[NotAuth]# auth test
send msg: AuthReq:type:"anonymous" passport:"user_fakeclienttest" password:"user_fakeclienttest"
recv msg: AuthAck //...
FC[Authed:281474976712031]#
FC[Authed:281474976712031]# char login 11
send msg: LoginReq: // ...
recv msg: LoginAck playerID:27113 // ...
FC[Logined:27113]#
FC[Logined:27113]#send HeartBeatReq {ClientTs:111}
send msg: HeartBeatReq:clientTs:111
recv msg: HeartBeatAck clientTs:111 serverTs:1597664509306
FC[Logined:27113]#
FC[Logined:27113]# self all
{"ID":27113,"name":"Newbie 27113", // ...

集成测试,压力测试,模拟测试,核心都需要一个模拟客户端,因此完全可以构建一套通用的fakeclient逻辑,包含基础网络通信,登录流程,数据状态同步等等。比如我们还基于fakeclient搭建了用于监控线上服务器可用性的监控机器人。

前面分别提到Q1,Q2的解决方案,至于Q3,我们的经验是,同学们之所以不愿意写测试,大部分原因都是测试框架还不够完善易用。另外,应该达成共识的是,开发效率并不只算单方面当前的开发时间,还应该包括客户端联调,QA验证反馈,后续重构负担等的时间,从这个角度来说,良好的测试规范起到的作用毋容置疑。


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK