Nodejs学习笔记之测试驱动


Posted in NodeJs onApril 16, 2015

分享第二章,关于测试驱动。这里的测试主要针对Web后端的测试 —— 你为什么要写测试用例(即测试用例的完善是否是浪费时间),如何完善你的测试用例,代码设计如何简化测试用例的书写,以及一些后期的构想。

1. 你为什么要写测试用例

这个习惯通常会被认为是一种耽误开发进度的行为,你需要花费几乎和开发代码相同的时间来逐步完善你的测试用例。但是在开发过程中,在开发完成一段代码后如果负责任而不是说完全把问题交给测试人员去发现的话,这个时候通常都会去做一些手动的测试。例如:

在代码中执行某些方法,查看输出的值是否符合预期。
修改数据库/缓存,然后执行某些方法,看数据库的变化是否符合预期。
使用工具模拟请求某些接口,查看接口的返回值/数据库的变化值是否会符合预期。
如果有前端页面的话,还会涉及到前后端联调,即要在前端页面上通过前端交互,查看前端的反馈是否符合预期,来间接验证后端代码的正确性。
现代化的测试工具都在尽可能的将这些人工的手动测试行为抽象成代码块,当你有意识去进行手动测试的时候,其实已经开始在尝试测试用例的行为了。既然可以通过手动的方式进行测试,那为什么还需要用代码来实现测试?

代码是可以复用或者在简单重构后可以实现更多的功能的,但是当你选择手动的时候,每次你都需要重头开始。
成熟的工作流中应当包括代码审核流程,代码审核的方式有很多,逐句阅读你的代码,或者检查你测试代码的完善性以及正确性,然后运行你的测试用例。后者更加简单。
当代码改动,例如修复 Bug 时候,很难保证你的改动是否会影响其他依赖你代码的部分。在人工测试的时代有一个叫做回归测试,即在你修复 Bug 将你的系统重新测试一遍。但是如果你已经有了完善的测试用例了呢,直接执行命令搞定。
当你重构代码的时候,同上。

2. 如何完善你的测试用例

在进入完善阶段前,先说说你将如何实现测试用例。

describe Meme do

 before do
  @meme = Meme.new
 end

 describe "when asked about cheeseburgers" do
  it "must respond positively" do
   @meme.i_can_has_cheezburger?.must_equal "OHAI!"
  end
 end

 describe "when asked about blending possibilities" do
  it "won't say no" do
   @meme.will_it_blend?.wont_match /^no/i
  end
 end
end

上面的代码来自于 Ruby 的 minitest。before 包含的代码块是在执行下面的测试用例前要做的事情,通常还会支持一个相对应的方法,在测试用例执行完执行。每个用例里面都进行一些很小的判断。

第一段中提到了一些手动测试里面经常会涉及到的测试内容,这里拿其中的 2 和 3 进行说明。在进行数据库相关的测试时,需要在 before 中插入一条测试数据,并且在 after 中删除测试数据。中间的测试用例中,通过执行相应的方法,执行完毕后:检查数据变化情况/检查是否有预期的异常/是否返回预期结果 来确认代码的正确性。如果是接口的话,就是通过代码发起对应的请求,然后检查返回的内容是否返回预期,有需要的话再去查看数据库里面的数据是否符合预期变化。

现在已经有了测试用例,但是任然需要考虑一种特殊情况。我现在为一个函数写了相对完善的测试用例了,跑完都 PASS 了,结果发现线上的日志里面还是有那个函数的报错。检查下发现函数的某个分支之前在测试的时候没有测试到,刚好线上的某种情况运行到了这个分支,结果有一个很不明显的语法错误报错了,有没有办法能确保所有的代码都测试过了?这里需要引入的是一个叫做 测试用例覆盖率 的概念,基本上每个语言都会有响应的实现。通过测试用例覆盖率,量化的告诉你你的测试用例有没有跑完某某文件里的所有代码,而你需要做的,就是尽可能保证你的覆盖率保持在 100%。

某种意义上来说,测试用例和测试覆盖率是用来提高开发者对自己代码自信心的工具。但是,他们也不是万能的。测试用例里面总可能会漏掉一些参数的可能性,当然你的代码里面也没有为这种可能性进行代码的编写,最终测试用例覆盖率只能告诉你你写的代码我们都帮你检测过了测试过了,对于你没有考虑到的可能性,表示无能为力。所以尽可能编写严格的代码,例如 javascript 里面尽可能都用 === 而不是 ==,使用强类型的编程规范等等,这些来降低这种因为接受的参数范围过大带来的潜在风险。

3. 代码设计如何简化测试用例的书写

整个 Web (也不局限于 web)通常包括三个层面的代码 —— 单纯数据处理与运算、涉及到数据库、涉及到具体的网络协议。其中单纯的数据运算于处理主要为普通的运算的函数或者是其他代码,涉及到数据库就是传统意义上 MVC 里面的 M,涉及到具体的网络协议就是对应的 C。这三块的测试分别对应着第一节中常规的测试内容的前三条。

因为 C层面通常还可能涉及到页面的渲染以及相应协议的模拟,所以通常把测试的重心放在函数以及数据库相关的代码里面可以减少测试用例代码的复杂度,这个就要求 Controller 的代码要尽可能少。对于复杂度较高的应用的一些目前的一些建议:

将数据的基础校验都放在 M层,如果使用 Ruby 开发的话,ActiveRecord以及Mongoid都提供了很方便使用的 validation 功能。
尝试在代码中使用 Pub/Sub 模式配合一些 ORM中提供的钩子(hook) 来实现 Model 之间的通信。 例如在 A 创建的时候发布某个消息,B监听到消息之后修改他自己的某个属性值。
使用 Command 模式将一些业务无关的功能从系统中抽离出来,例如邮件发送。
以上建议参考:Laravel wisper resque

4. 构想

以上的内容都避开了前后端需要联调的测试用例,下面的内容主要是针对这块。Ruby 在这个方向已经有一些比较优雅的实现,感兴趣的可以直接先去欣赏一下 Capybara。

随着包括 Selenium Phantomjs 以及基于前者的 Watir 等一系列浏览器驱动的普及,使用代码控制浏览器已经不再是一件很复杂的事情。在这个能力的基础上,可以尝试把基于前端的测试分为四步:

等待某标志性元素出现(例如等待页面载入玩,或者某个内容异步加载出现)
模拟用户操作,这里的操作包括且不局限于用户点击、用户输入
等待反馈中标志性元素出现(例如某某输入框出现)
判断内容,是否符合预期
基于这个流程,可以解决绝大多数的前端测试。但是单纯依靠这个流程任然不够,因为页面中可能出现例如验证码这样的阻碍元素,在不修改代码的前提下,可以尝试通过数据库/缓存来取到这些内容。同样,和测试接口相同,这里也涉及到在测试前数据库中插入测试数据,测试用例执行后严重数据库里面数据变化,以及全部测试完毕后删除测试数据的内容。最终导致这块测试用例代码的实现需要同时对前端后端有一定的了解。目前还在考虑在借鉴 Capybara 的基础上,设计出更加通用的方案。

最后贴一段 Capybara 的代码结束这段内容:

feature "Signing in" do
 background do
  User.make(:email => 'user@example.com', :password => 'caplin')
 end

 scenario "Signing in with correct credentials" do
  visit '/sessions/new'
  within("#session") do
   fill_in 'Email', :with => 'user@example.com'
   fill_in 'Password', :with => 'caplin'
  end
  click_button 'Sign in'
  expect(page).to have_content 'Success'
 end

 given(:other_user) { User.make(:email => 'other@example.com', :password => 'rous') }

 scenario "Signing in as another user" do
  visit '/sessions/new'
  within("#session") do
   fill_in 'Email', :with => other_user.email
   fill_in 'Password', :with => other_user.password
  end
  click_button 'Sign in'
  expect(page).to have_content 'Invalid email or password'
 end
end
NodeJs 相关文章推荐
nodejs分页类代码分享
Jun 17 NodeJs
基于NodeJS的前后端分离的思考与实践(五)多终端适配
Sep 26 NodeJs
nodejs URL模块操作URL相关方法介绍
Mar 03 NodeJs
Nodejs从有门道无门菜鸟起飞必看教程
Jul 20 NodeJs
详解如何在NodeJS项目中优雅的使用ES6
Apr 22 NodeJs
在Debian(Raspberry Pi)树莓派上安装NodeJS的教程详解
Sep 19 NodeJs
NodeJS实现不可逆加密与密码密文保存的方法
Mar 16 NodeJs
基于nodejs res.end和res.send的区别
May 14 NodeJs
nodejs更新package.json中的dependencies依赖到最新版本的方法
Oct 10 NodeJs
Nodejs让异步变成同步的方法
Mar 02 NodeJs
nodeJs项目在阿里云的简单部署
Nov 27 NodeJs
node快速搭建后台的实现步骤
Feb 18 NodeJs
Nodejs学习笔记之入门篇
Apr 16 #NodeJs
Windows系统下使用Sublime搭建nodejs环境
Apr 13 #NodeJs
nodejs开发微博实例
Mar 25 #NodeJs
nodejs中实现阻塞实例
Mar 24 #NodeJs
nodejs中使用多线程编程的方法实例
Mar 24 #NodeJs
nodejs中实现sleep功能实例
Mar 24 #NodeJs
nodejs中的fiber(纤程)库详解
Mar 24 #NodeJs
You might like
php获取某个目录大小的代码
2008/09/10 PHP
如何利用http协议发布博客园博文评论
2015/08/03 PHP
php测试kafka项目示例
2020/02/06 PHP
两种WEB下的模态对话框 (asp.net或js的分别实现)
2009/12/02 Javascript
浅谈关于JavaScript的语言特性分析
2013/04/11 Javascript
Extjs4实现两个GridPanel之间数据拖拽功能具体方法
2013/11/21 Javascript
Node.js(安装,启动,测试)
2014/06/09 Javascript
Jsonp 关键字详解及json和jsonp的区别,ajax和jsonp的区别
2015/12/30 Javascript
实现JavaScript高性能的数据存储
2016/12/11 Javascript
jQuery弹出窗口简单实现代码
2017/03/09 Javascript
源码分析Vue.js的监听实现教程
2017/04/23 Javascript
使用Node.js实现RESTful API的示例
2017/08/01 Javascript
Vee-Validate的使用方法详解
2017/09/22 Javascript
layui 表格的属性的显示转换方法
2018/08/14 Javascript
浅析Vue.js 中的条件渲染指令
2018/11/19 Javascript
bootstrap-table formatter 使用vue组件的方法
2019/05/09 Javascript
JS正则表达式封装与使用操作示例
2019/05/15 Javascript
layui文件上传控件带更改后数据传值的方法
2019/09/23 Javascript
vue v-for出来的列表,点击某个li使得当前被点击的li字体变红操作
2020/07/17 Javascript
Python中的map、reduce和filter浅析
2014/04/26 Python
Python协程的用法和例子详解
2017/09/09 Python
django2 快速安装指南分享
2018/01/05 Python
Python搭建FTP服务器的方法示例
2018/01/19 Python
python 实现调用子文件下的模块方法
2018/12/07 Python
Python实现朴素贝叶斯的学习与分类过程解析
2019/08/24 Python
Python的轻量级ORM框架peewee使用教程
2021/02/05 Python
乌克兰移动电子产品和相关配件的在线商店:iTMag
2020/03/16 全球购物
岗位职责说明书
2014/05/07 职场文书
面试必备的求职信
2014/05/25 职场文书
2015年宣传部部长竞选演讲稿
2014/11/28 职场文书
综合素质评价思想道德自我评价
2015/03/09 职场文书
关于应聘教师的自荐信
2016/01/28 职场文书
MySQL kill不掉线程的原因
2021/05/07 MySQL
如何用python清洗文件中的数据
2021/06/18 Python
深入浅析Django MTV模式
2021/09/04 Python
动画《朋友游戏》公开佐藤友生绘制的开播纪念绘
2022/04/06 日漫