有webservice参与的系统的单元测试,最好使用mock object

访问量: 647

手头上的一个项目,是以另外一个系统的webservice做为底层基础。

里面大约有50的操作,最终都要调用这些 web service。

大约有200个test case, 跑完一边居然要15~ 30分钟。因为调用一次WS,大约25秒左右。而且随着远程webservice 服务器的性能问题,这个时间还在增加。

程序员感觉很麻烦。因为调试的时候,如果远程 Webservice出了问题,本地也运行不了。

而且从过去的经验来看,这个webservice 不是很稳定。大约有3~ 5% 的出错率(网络原因造成)。很多时候需要跑很多次,才会明确结果。

所以这个项目越到后来,程序员就越不乐意运行单元测试, 草草的改了代码,最多运行一下对应的 model/controller spec, 然后提交。 最近一个月几乎没有跑过全部的webservice.  所以感觉非常麻烦。

另外一个原因, webserivce 的测试不好写。 不想test database那样可以轻易rollback,  webservice 上的操作都需要手动的回复。某个 test case 提交一个POST增加测试数据,那么就必须在这个test case运行完之后删掉对应的数据。如果运行顺利还好说,不顺利的话,程序遇到了异常,直接跳过“删除对应数据”这一步,直接跳到下一个。

难于测试的程序就难于开发,所以项目 就越来越难不好做。

所以今天开始,决心引入mock object,最大限度的解决这个问题。

原则就是:

The mock object should also have exactly the same methods(public) that the original object has.

步骤: 视系统而定。有的系统是java,有的是rails, 这个系统是rails/rspec, 所以...

先找到一个最核心的resource(调用远程webservice的文件),然后mock it!~~~

1. 向 spec/support 中增加 一个mock object:     mock_device_resource.rb   ,(它所模拟的就是 device_resource.rb )

原则上它的接口跟 original object是一样的,不过由于我的程序有点儿复杂,所以我为它们增加了一个新参数: options. 例如:

原始的对象:
# original object
class DeviceResource < BaseResource
  def initialize(cloudset)
  end
  def find(params)
    ...
  end
end


模拟的对象:
# mock objects
# to inherite the Constants from ServerSettingResource
class MockServerSettingResource < ServerSettingResource
  # do nothing, just compatible with the interface
  def initialize(cloudset)
  end 

  # +options+
  # * :failed :  if set to true, will return [].  default is false
  # * :blank_result : alias to : failed option
  def find(params, options = {}) 
    return [] if options[:failed] || options[:blank_result]
    return [result_hash.merge(params)]
  end 
end


2. 建立这个mock object 的 rspec 文件, 把 原来的 device_resource_spec中的所有test case, 都COPY过去。

3. 运行测试,增加实现。。。直到 mock_device_resource_rspec.rb 的测试全部通过。

至此,mock_device_resource.rb 已经通过TDD的方式实现好了。在我这里看到的改善是(消耗的时间):

  testcase1:   45 s =>0.x

  testcase2:   15s => 0.x

  testcase3:  51.4 => 0.x

  testcase4: 25 =>0.0x

时间被大大缩短,原来需要 200秒的测试,现在 0.45s .  爽歪歪了。


4. 那么,那些调用真实存在的webservice 的test case,对象,改如何处理呢? 对于 original object, 进行一些预处理:加上一个调用时的参数,来跳过这个 spec 文件:
describe DeviceResource do
  next if ENV['with_real_webservice'] != "true"
  before do
    #...
  end 
end

然后,调用的时候:
4.1 跳过该文件: $ bundle exec rspec spec
4.2 执行该文件: $ with_real_webservice=true bundle exec rspec spec

5. 事情还没完,接下来,我们要找出所有origin object被调用的地方,使用 mock object “尽量(因为还是有一些test case很难替换,或者这些test case测试的就是的 web service,所以,能替换多少就多少吧)”替换这些 original object.

6. 最后,修改spec_helper.rb,使用所有的 mock object屏蔽掉它们的original object.
# 因为 类名 实际上就是 Constants 
if ENV['with_real_webservice'] != "true"
  DeviceResource = MockDeviceResource
  DifsSettingResource = MockDifsSettingResource
  ServerSettingResource = MockServerSettingResource
end


订阅/RSS Feed

Subscribe

分类/category