惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

G
GRAHAM CLULEY
T
Tenable Blog
Know Your Adversary
Know Your Adversary
C
CXSECURITY Database RSS Feed - CXSecurity.com
P
Privacy International News Feed
S
Security Affairs
NISL@THU
NISL@THU
O
OpenAI News
Attack and Defense Labs
Attack and Defense Labs
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Hacker News: Ask HN
Hacker News: Ask HN
Webroot Blog
Webroot Blog
Schneier on Security
Schneier on Security
S
SegmentFault 最新的问题
S
Schneier on Security
G
Google Developers Blog
V
V2EX
C
Check Point Blog
U
Unit 42
Google DeepMind News
Google DeepMind News
T
Threatpost
阮一峰的网络日志
阮一峰的网络日志
T
The Exploit Database - CXSecurity.com
Recent Announcements
Recent Announcements
M
MIT News - Artificial intelligence
S
Secure Thoughts
博客园 - 司徒正美
Recorded Future
Recorded Future
P
Proofpoint News Feed
Spread Privacy
Spread Privacy
K
Kaspersky official blog
Cyber Security Advisories - MS-ISAC
Cyber Security Advisories - MS-ISAC
AI
AI
博客园 - 聂微东
N
News and Events Feed by Topic
SecWiki News
SecWiki News
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
V
Vulnerabilities – Threatpost
P
Palo Alto Networks Blog
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
Engineering at Meta
Engineering at Meta
Recent Commits to openclaw:main
Recent Commits to openclaw:main
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
酷 壳 – CoolShell
酷 壳 – CoolShell
WordPress大学
WordPress大学
The Hacker News
The Hacker News
The Last Watchdog
The Last Watchdog
Project Zero
Project Zero
W
WeLiveSecurity
博客园 - Franky

重归混沌的BLOG

给silly实现了一个ernro模块 | 重归混沌的BLOG 给silly实现了一个ernro模块 | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG 第一次在生产环境使用 Vibe Coding | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG API 设计的艰难抉择 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 十年 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 在Go语言中如何使XML加载内存无限趋近于0 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG 对跨服玩法中的分布式一致性问题进行简单抽象 | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG Go语言逃逸分析之slice和map | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 谈谈观测 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 写了个AI Agent服务端 | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 谈谈代码设计中“严丝合缝” | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 | 重归混沌的BLOG 一次艰难的线上游戏服务器内存排查经历 如何基于LanguageServerProtocol来编写lint工具 谈谈游戏服务器中RPC模块的设计 谈谈游戏服务器代码抽象 最近碰到的一个分布式一致性问题 对Raft协议的一点理解 使用mmap来学习/proc/pid/smaps 2023(完) 再次实现了一个Lua性能分析器 终于给Silly的定时器增加了取消功能 一次虚拟内存排查经历 游戏服务器分布式数据的一种同步的思路 为silly增加了互斥锁 2022(完) Go语言之闭包篇 一例误用unsafe包引起的内存问题 Go语言之内存篇 初识Go语言 重新抽象图形API 给Lua实现了一个数学库 谈谈跨平台图形API的抽象 寻路和Flocking算法的结合 行为树的一种高效实现 内测过程中Shader出现的问题 彻底解决多国语言 谈谈数据库的选型 再谈Lua热更新(终) 初窥Rust 关于游戏服务器的服务拆分 ECS的初步实现 ECS初探 屏幕空间(SreenSpace)的想象力 一些对辐射度量学的理解 深度缓冲和半透明渲染 Mysql的间隙锁 更新一些GPU相关知识 2020 地形渲染之爬过的坑 Lua5.3 GC源码阅读(5) 实现一个数据库存储队列 再学计算机图形学入门 再谈分布式服务架构 游戏上线一个月后的反思 一次并发Bug 双向链表的三种实现 谈谈随机数的使用 再谈性能优化 2019 Lua中的函数式编程 重构登录逻辑 Unity资源管理(续) 谈谈Unity的资源管理 一次关于Cache的性能分析 历史之2018 DC3算法 移动平台native代码遭遇的坑 从CPU层面谈谈优化 开卷有益(UNIX编程艺术篇) GC竞争问题 通过Mesh投影来实现贴花系统 谈谈我对数据同步的理解 又一个类型提升引起的Bug Lua5.3 GC源码阅读(4) Lua5.3 GC源码阅读(3) Lua5.3 GC源码阅读(2) Lua5.3 GC源码阅读(1) 三角形光栅化时遇到的坑 一次git事故 再见2017 又一个lua调试器 客户端缓存落地方案 Paxos算法 HTTP服务器的特点 一次性能优化经历 关于CPU分支预测 C程序中让两个不同版本的库共存 实现了一个AOI模块 一个高可伸缩的游戏服务器架构 关于网络协议封装的一些新想法
谈谈游戏服务器的自动化测试
重归混沌 · 2024-07-07 · via 重归混沌的BLOG

当我们要测试一个’算法’是否正确时,常常会打开代码编辑器为其编写测试代码。

这种测试往往被称为单元测试,即测试相对独立的最小单元。

由于算法往往都是一个独立的个体,没有依赖,因此很容易为其编写测试代码。

这里的“容易”指的是,我们除了设计testcase之外,不会有额外的心智负担。

当我们开始为业务逻辑编写测试代码时,其复杂程度往往会是我们放弃的开始。

举个例子,class A依赖class Bclass C

当我们为class A编写测试代码时,为了解决class Bclass C对测试结果的干扰,或者验证class A执行结果的正确性,往往需要为class Bclass C编写mock class。

这意味着我们在每写一个class的同时,也需要维护至少一个对应的mock class,以方便依赖的模块来编写测试代码。

在一些编程语言中,还意味着我们需要为每个class提供一个interface的定义。即,class A依赖的是interface Binterface C,而class Bclass C只是interface Binterface C的实现。不然无法实现测试时注入mock class的需求。

我简单尝试了一下,很快便放弃了。

一是因为它对业务有侵入性,需要强制定义interface;二是因为我认为,为每个class维护一个mock class,工作量太大了。

后来我尝试对这种测试思路做出一些简化。

在编写测试代码之前,我会先人工分析出这些class之间的依赖关系。先为依赖链中的末端class编写测试代码。

当某个class的测试代码成功执行后,就认为这个模块是正确的。然后再依次向上进行测试,直到测试完依赖链的顶端。

在这个过程中,我不会提供mock class,而是直接读取到所需要的class中的内部状态来验证结果是否正确。

以上面的依赖关系为例,我会先为class Bclass C编写测试代码。

当我认为class Bclass C没有bug时,就会开始为class A编写测试代码。

为了验证class A结果是否正确,我会直接读取class Bclass C的内部状态来验证。

但这依然不足以让我为游戏服务器编写测试代码,因为游戏服务器是有状态的,而且依赖关系远比我想象的更复杂。

有一天,我突然灵光一闪,想看看luaredis是怎么来做测试的。

我发现它们也没有mock class(这里的class仅代表某个代码单元,不局限于语法)。

但是作为互联网开源软件,它们的测试是非常到位的,不然无法保证质量。

它们采用的方式都很类似,都是针对特性进行测试,而不是某个函数或class

lua作为语言虚拟机,它是没有API的,但为了帮助测试,它会在测试模式下导出一些API用于感知当前luaVM的内部状态。

redis作为一个单独的服务,它是从协议层面去测试每一个特性的。

这给了我很大的启发。我想游戏服务器也是需要从协议层面去测试所有功能的,虽然这样代码覆盖率没有单元测试那么高,但想必对代码质量也是有足够帮助的。

既然通过协议来对游戏服务器进行测试,那就势必要启动真正的游戏服务器,这就会导致更多的外部依赖:时间配置数据库

  • 时间会要求我们在测试某个功能时,服务器时间必须处于某个指定的时间区间。

  • 配置会要求我们在测试某个功能时,某个配置必须包含多少条数据或什么数值,以便测试一些corner case

  • 数据库会有持久化问题,因此某个功能测过一遍之后,状态就被保存下来,下次就无法再次触发了。

这些外部依赖无一不在揭示着“测试代码不能重复执行”这一问题。

为了解决这个问题,我当时的设想是,每测试一个功能就将数据库清空,然后时间修改到本功能所需要的时间,同时为每一个testcase保留一份配置用于测试。

但随之而来的问题就是,因为清档了,所以每执行一个测试用例,都必须要创建新号。

而测试A模块时,它可能对B模块的数据有依赖,否则协议就无法成功执行。

我最初的想法是,在测试代码库中提供很多辅助条件函数。

随着testcase的增加,需要新增的辅助函数将会越来越少。

比如我要测试A模块,它需要玩家等级达到35级,那么就有一个辅助函数,它可以将玩家升到指定等级(是通过正常协议途径,而不是GM直接修改,因为升级有可能还会触发连带效应)。

后面再依赖玩家等级的testcase就不需要再编写这个辅助函数了。

这虽然很麻烦,但理论上它是完备的,可以测试任意代码。

为了验证这一想法,我挑了一个SLG的核心地图玩法,为其编写了测试代码,来证明它的可行性。

事实证明,这确实是可行的,但也确实是麻烦的。

因为我不仅遇到了为玩家升级,还碰到了为英雄升级,为技能升级,装备技能,操作队伍等一系列的测试辅助需求。

我想朝着这个方向继续简化,却一直没有头绪。


随着这两年我对游戏服务器理解的加深,以及见到了不一样的GM创号思路,我发现我终于可以简化这一流程了。

首先,我之前为每个testcase保存一份配置表是没有必要的。

一般来讲,每个模块只会需要有限的几个表或几行数据。

我们只需要在测试框架中提供修改配置表的能力即可。

在运行测试代码前,可以直接将配置改成测试代码需要的数据即可。

其次,为了解决模块间的数据依赖,使用辅助函数达到本模块要求是可行的,但不是必要的。

据我这两年的观察和反思,模块间的依赖其实没有那么紧密,大部分情况下只要分为达到达不到即可。

比如我需要玩家等级为30级,那么100级也是满足的。

因此提供一个GM指令,创建一个足够高级的号就足够了。

如果恰好我们需要测试的模块也被高级号给初始化了,我们的testcase代码可以直接操作数据库,清空这部分数据,然后重新登录即可。

lua在测试模式下导出一些API用于感知当前luaVM的内部状态,也给了我一些启示。

我们可以增加一个指定GM指令用来获取玩家或全服的状态,用于我们在测试代码中感知游戏服务器进程中的准确状态。

如果游戏服务器采用了分布式技术,还需要提供一个透传GM指令,用于获取分布式系统中某个服务进程中的状态。

除此之外,测试框架还需要提供重启集群和动态修改时间的能力。

总结如下,测试框架只需要提供如下能力即可满足绝大部分测试需求:

  1. 创建高级号的能力

  2. 直接访问数据库的能力

  3. 修改配置文件的能力

  4. 修改服务器时间的能力(包括进程运行时修改)

  5. 重启集群的能力,用于每测试一个用例,都清档重启

  6. 感知集群内进程状态的能力

至此,经历七年的思考,我认为这个测试方案终于达到了可以实践的阶段。

ps. 本文是我在实践2个月之后写出的,基本上可以确定心智负担极小。

pps. 让我们将视野拉高,由于每次测试都将数据库清空,因此数据库中的数据也可以等价于内存结构。与此同时,很多单元测试的技巧也可以拿来使用。比如要保持测试用例尽可能的小(一旦测试用例过于复杂,测试用例的正确性就不能得到保证)等。