最佳实践:使用NodeJS进行web开发,如何优雅地进行数据层的单元测试?
仔细想了下,觉得我问的这个问题维度可大可小,我就列一下我的一些困扰吧,求给建议或者分享经验。
如何确定测试的维度?
以一个常见的web应用为例,我们一般会有数据层(完全对数据库的操作封装),面向客户端的业务接口,可能还会有面向第三方应用的API。那么如此一般你们会对那些维度进行测试呢?比如:
- 数据层做单元测试?
- 针对API做http接口测试?
纠结主要在于虽然是不同层的测试,但是其实有非常多得荣誉在里面。比如数据层的一个
createUser
方法的测试,对于API的HTTP层,无非可能直接就是把用户输入传到这个方法里面,然后返回这个方法的结果给客户端。
测试的具体编写你们一般怎么搞?
目前来说这些测试我都是手写,不知道大家是否有更加自动化的方式做这些测试。手写的话,我个人感觉问题有:
-
机械重复劳动:比如你的业务中有
user
,app
,project
这些事例,那么这三者的所谓 "增删改查" 的用例基本上千篇一律,可能你写完user
,后面的app
和project
基本上就是复制过来然后改改名字了。 - 模拟数据构造的繁琐
- 缺少最佳实践的指导,比如下面这样的情况:
js
describe( 'common', function(){ it( '添加用户', function( done ){ done(); }); it( '获取用户列表', function( done ){ done(); }); it( '获取某个用户', function( done ){ done(); }); it( '更新用户', function( done ){ done(); }); it( '删除用户', function( done ){ done(); }); });
感觉应该是比较常见的case,那么除了第一个 “添加用户” 外,可以看到,其他得所有case都需要 “某个用户已经被添加” 为前提。那么一般大家是怎么操作呢?我想到的思路有两种:
-
方案一:在每个case里面自己独立 添加一个新用户,结束的时候再删除掉这个用户(可以使用mocha的
beforeEach
和afterEach
) - 方案二:创建一个全局的新用户变量,在第一个case跑完后,复制到这个变量中,后续的case就直接使用这个变量即可
欢迎发散分享经验!!!!
Answers
按照我的经验:
- 测试要以「业务流程」为单位,而不是以「接口」为单位
- 不同的测试之间不要共享数据,尽可能不要使用全局变量
- 业务逻辑尽量往 Model 里写(或者单独抽象出一个 Service 层),不要写在 Controller
- 如果发现重复的测试比较多的话,可以先不写测试(或只对其中一个地方写测试),等到发现某个功能有问题,再针对问题写测试
所谓「业务流程」就是指类似「一个普通用户注册帐号、登录、修改密码、发帖、回复」或者「一个管理员账户登录管理员面板、发帖、回复、删除别人的发帖」的流程。这样的好处有很多,比如前面第一个流程的每个测试之间都共享同一个「普通用户」,第二个流程都共享一个「管理员用户」。
在一个流程内部共享数据可控性比较强,因为一个流程一般不会太长,而且内部的联系是比较大的。如果针对每个接口做测试的话,要么为每个测试单独准备数据(很繁琐),要么在所有测试之间共享数据(会出现很多全局变量,会很大程度上增加复杂度,测试之间互相还可能出现干扰)。
应该保证每一个测试文件里的测试是独立的,用 before 来定义这个文件所依赖的数据(比如这个文件的所有测试都需要先有一个用户),你可以在其他文件定义一个
createTestAccount
的函数,接受一些选项,然后来生成符合要求的测试数据。这样的好处就是你可以单独运行某个测试,如果运行整个测试比较耗时的话,这样可以解决很多时间,而且你也不必担心测试之间会互相干扰。而且比如 mocha 这样的库,它是不担保不同测试文件之间的运行顺序的(虽然实际会按字母顺序运行),如果不同的测试文件之间有依赖会很麻烦。
至于究竟是测 API 接口,还是测 Model, 这个就比较见仁见智了,不过「尽量把业务逻辑写到 Model」里这点是不会变的。
如果是测 API 的话,那么测试可以直接调用 Model 里的方法来准备数据和验证测试结果(比起用 API 来准备数据和验证结果,可是要方便多了);如果是测 Model 的话,因为大部分逻辑在 Model 里,所以 Model 测完就可以基本保证没有大问题了,因为 Controller 里的逻辑不多。
最后,请一定不要用「代码生成器」来生成测试代码。能够用脚本生成,说明这些测试之间是有内在的逻辑的,你完全可以通过好的设计来避免出现重复的代码,毕竟 mocha 的测试也都是 JavaScript 代码,有什么不能实现的呢?当然,测试的抽象程度不能太高了,要稍微直白一点,否则就会在「调试测试」上花太多时间,这之间需要自行做个权衡。
关于「代码生成器」请参考 程序员修炼之道:从小工到专家 一书中的「邪恶的向导」一节,这本书中亦介绍了大量编写自动测试的技巧。