近半年在做分布式系统开发的同时,也做了不少的
测试工作,软件工程教科书上描述软件项目的流程,基本上都会提到
单元测试、集成测试、压力测试等名词,但对这些词汇一直停留在理论认识阶段。研究生阶段做的项目,因为要求不高,基本上也没做什么测试工作;去年在实习的时候,因为时间有限,主要接触单元测试和系统
功能测试;直到现在才把这些词汇都近距离的感受了一下。
单元测试
分布式系统的开发工作通常会被划分成多个模块,由不同的开发人员分别编写程序,所以代码的单元测试工作通常是针对单个模块进行的。如果模块是独立的,并 且功能集足够小,单元测试是很容易做的,构造一组case,尽量覆盖所有的分支基本上就OK。但实际上分布式系统里很少有完全独立的模块,大部分的模块都 会跟其他的模块有依赖关系或是网络通信等。
对于有依赖的模块的单元测试,理想情况下,依赖的模块都已经准备好,并且被测试过没有问题 (这个实际上是做不到的,而且模块间有时还会存在相互依赖的情况),这种理想情况会严重影响开发效率,使得有依赖的模块就只能串行开发测试。另外,如果依 赖的模块在安装、部署上需要花费很长的时间(比如是一个配置比较麻烦的server),那么每次单元测试都需要把server部署起来,测试成本是很高的。
实际单元测试的过程中,我们经常会把依赖其他模块的地方用简单的代码代替,也就是做mock。最直观的mock方式就是通过宏来控制,即如果是以 DEBUG模式运行,执行某段简单的代码(mock),如果非DEBUG模式运行,则执行实际的代码逻辑。比如通过下面一段代码把“从网络上获取数据”在 DEBUG模式运行时替换为“从本地内存获取数据”,那么在执行单元测试时,我们就不需要依赖网络的对端来提供数据,从而方便的测试代码逻辑。而实际 fetch_data_from_network()的测试可延迟到功能测试阶段。
#ifndef DEBUG_MODE fetch_data_from_network(); #else fetch_data_from_local_memory(); #fi |
使用宏来控制有时会使得代码读起来很混乱,如果是使用C++开发(或其他面向对象编程语言),更好的方式是借助多态的性质来做mock,如下面一段代 码,DataManager是一个负责管理数据的类,它需要从网络上其他的服务里获取数据,MockDataManager是一个继承自 DataManager的类,它从本地内存获取数据,在测试时,我们可以将DataManager的实例换成MockDataManager的实例来运行 (必须是指针或是引用),这样思路跟前面的思路其实是一样的,只不过借助多态,更清晰明了,需要做的事情更少。google test和google mock是开源的测试、以及mock框架,使用他们会使你的测试工作更简单,更有趣。
class DataManager { public: DataManager(); ~DataManager(); virtual int fetch_data() { fetch_data_from_network(); } ... }; class MockDataManager : public DataManager { public: virtual int fetch_data() { fetch_data_from_local_memory(); } }; |
测试久了之后,你会发现测试的工作量往往跟代码结构的设计有很大的关系,如果代码结构本身毫无章法,再加进去一堆为单元测试而写的代码逻辑,只会令代码 看起来更加糟糕;如果设计之初就考虑测试需求,尽量把业务与逻辑层次分开降低模块间依赖性,把可能需要mock测试的地方设计为虚基类等,在做测试的时候 需要做的工作就会很少,而且测试新增代码不会打乱现在的代码逻辑结构。