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 npm包管理的配置方法及常用命令介绍
Jun 05 NodeJs
Nodejs sublime text 3安装与配置
Jun 19 NodeJs
nodejs的10个性能优化技巧
Jul 15 NodeJs
Express与NodeJs创建服务器的两种方法
Feb 06 NodeJs
深入nodejs中流(stream)的理解
Mar 27 NodeJs
详解使用nodeJs安装Vue-cli
May 17 NodeJs
基于nodejs+express4.X实现文件下载的实例代码
Jul 13 NodeJs
让nodeJS支持ES6的词法----babel的安装和使用方法
Jul 31 NodeJs
NodeJS安装图文教程
Apr 19 NodeJs
解决nodejs的npm命令无反应的问题
May 17 NodeJs
Nodejs对postgresql基本操作的封装方法
Feb 20 NodeJs
NodeJs 模仿SIP话机注册的方法
Jun 21 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
星际流派综述
2020/03/04 星际争霸
PHP读取目录下所有文件的代码
2008/01/07 PHP
简要剖析PHP的Yii框架的组件化机制的基本知识
2016/03/17 PHP
优化网页之快速的呈现我们的网页
2007/06/29 Javascript
JavaScript 读取元素的CSS信息的代码
2010/02/07 Javascript
javascript 拖动表格行实现代码
2011/05/05 Javascript
Javascript中克隆一个数组的实现代码
2013/12/06 Javascript
javascript检测是否联网的实现代码
2014/09/28 Javascript
CSS中position属性之fixed实现div居中
2015/12/14 Javascript
JavaScript 函数的执行过程
2016/05/09 Javascript
javascript类型系统——日期Date对象全面了解
2016/07/13 Javascript
vue中渐进过渡效果实现
2016/10/27 Javascript
jQuery中的select操作详解
2016/11/29 Javascript
vue.js树形组件之删除双击增加分支实例代码
2017/02/28 Javascript
jQuery实现贪吃蛇小游戏(附源码下载)
2017/03/04 Javascript
jquery mobile实现可折叠的导航按钮
2017/03/11 Javascript
详谈jQuery中使用attr(), prop(), val()获取value的异同
2017/04/25 jQuery
详解Angular2组件之间如何通信
2017/06/22 Javascript
Vue.js图片预览插件使用详解
2018/08/27 Javascript
vue click.stop阻止点击事件继续传播的方法
2018/09/04 Javascript
解决Vue使用swiper动态加载数据,动态轮播数据显示白屏的问题
2018/09/27 Javascript
Javascript实现一朵从含苞到绽放的玫瑰
2019/03/30 Javascript
Vuex持久化插件(vuex-persistedstate)解决刷新数据消失的问题
2019/04/16 Javascript
JS学习笔记之闭包小案例分析
2019/05/29 Javascript
解决vue动态路由异步加载import组件,加载不到module的问题
2020/07/26 Javascript
原生js+canvas实现下雪效果
2020/08/02 Javascript
Python中MySQL数据迁移到MongoDB脚本的方法
2016/04/28 Python
速记Python布尔值
2017/11/09 Python
Python读取txt文件数据的方法(用于接口自动化参数化数据)
2018/06/27 Python
Django框架 Pagination分页实现代码实例
2019/09/04 Python
使用javascript和HTML5 Canvas画的四渐变色播放按钮效果
2014/04/10 HTML / CSS
建筑公司文秘岗位职责
2013/11/29 职场文书
股东协议书
2014/04/14 职场文书
服务承诺口号
2014/05/22 职场文书
小学四年级作文之人物作文
2019/11/06 职场文书
win10系统计算机图标怎么调出来?win10调出计算机图标的方法
2022/08/14 数码科技