RSpec如何mock支持all!方法的ZendeskAPI::Collection对象
如何Mock支持
.all!方法的ZendeskAPI::Collection响应 问题场景
尝试mock如下调用链路的Zendesk API响应:
zendesk_client = ZendeskHelper.make_client tickets = zendesk_client.search.export(query)
业务代码通过Zendesk API提供的.all!方法遍历全部分页结果:
tickets.all! do |ticket| # 业务处理逻辑 end
最初的mock实现是将测试double存入数组,将数组作为mock响应返回:
mock_tickets = [ double('ticket'), double('ticket'), double('ticket') ] allow(mock_zendesk_client).to receive_message_chain(:search, :export).and_return(mock_tickets)
运行测试时报错:普通Ruby数组没有实现.all!方法,无法响应该调用。
排查确认真实接口返回的tickets对象类型为ZendeskAPI::Collection,需要mock该类型对象使其支持.all!方法正常调用。
附.all!方法的核心实现源码:
def all!(start_page = @options["page"], &block) _all(start_page, :bang, &block) end def _all(start_page = @options["page"], bang = false, &block) raise(ArgumentError, "must pass a block") unless block page(start_page) clear_cache while (bang ? fetch! : fetch) each do |resource| arguments = [resource, @options["page"] || 1] if block.arity >= 0 arguments = arguments.take(block.arity) end block.call(*arguments) end last_page? ? break : self.next end page(nil) clear_cache end
解决方案
根据测试场景的复杂度,可选择以下两种实现方式:
方案1:轻量mock(单一遍历场景首选)
无需构造完整的
ZendeskAPI::Collection实例,仅需让mock返回的对象响应.all!调用,按预期将mock ticket逐个传入执行块即可:mock_tickets = [ double('ticket'), double('ticket'), double('ticket') ] # 构造支持all!方法的mock集合对象 mock_collection = double('ZendeskAPI::Collection') allow(mock_collection).to receive(:all!) do |&block| mock_tickets.each { |ticket| block.call(ticket) } end allow(mock_zendesk_client).to receive_message_chain(:search, :export).and_return(mock_collection)该写法逻辑最简,无额外依赖,完全适配当前仅通过
.all!遍历资源的场景。如果业务代码用到了.all!传入的第二个页码参数,只需调整块内传参即可:allow(mock_collection).to receive(:all!) do |&block| mock_tickets.each { |ticket| block.call(ticket, 1) } end方案2:真实Collection实例存根(复杂交互场景适用)
如果测试逻辑中还需要用到Collection的分页判断、翻页等其他原生方法,可直接实例化真实的
ZendeskAPI::Collection对象,存根其内部拉取逻辑绕过真实HTTP请求:mock_tickets = [ double('ticket'), double('ticket'), double('ticket') ] # 初始化真实Collection实例,传入mock client和关联资源类型 mock_collection = ZendeskAPI::Collection.new( mock_zendesk_client, ZendeskAPI::Ticket, path: 'search/export' ) # 存根内部拉取逻辑,模拟第一页拉取完成即到达最后一页 allow(mock_collection).to receive(:fetch!) do # 将mock tickets注入集合实例变量,使each方法可正常遍历 mock_collection.instance_variable_set(:@resources, mock_tickets) allow(mock_collection).to receive(:last_page?).and_return(true) true end allow(mock_zendesk_client).to receive_message_chain(:search, :export).and_return(mock_collection)该方案下Collection所有原生方法均可正常调用,无需逐个mock,适配后续有更多集合交互的测试场景。
注意:不建议通过给数组混入模块、猴子补丁添加
.all!方法的方式实现,容易引入意外的状态依赖,导致测试结果不稳定。
内容的提问来源于stack exchange,提问作者Panpaper
相关产品推荐
相关产品推荐

