有webservice参与的系统的单元测试,最好使用mock object
访问量: 2318
手头上的一个项目,是以另外一个系统的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. 例如:
原始的对象:
模拟的对象:
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 文件:
然后,调用的时候:
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.
里面大约有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