【问题标题】:mocking controller methods in rspec在 rspec 中模拟控制器方法
【发布时间】:2014-11-26 07:38:07
【问题描述】:

(这个问题类似于Ruby on Rails Method Mocks in the Controller,但使用的是旧的stub 语法,而且没有得到有效的答案。)

短格式

我想将我的控制器代码与我的模型代码分开测试。不应该是 rspec 代码:

expect(real_time_device).to receive(:sync_readings)

确认RealTimeDevice#sync_readings 被调用,但禁止实际调用?

详情

我的控制器有一个调用RealTimeDevice#sync_readings 的#refresh 方法:

# app/controllers/real_time_devices_controller.rb
class RealTimeDevicesController < ApplicationController
  before_action :set_real_time_device, only: [:show, :refresh]
  <snip>
  def refresh
    @real_time_device.sync_readings
    redirect_to :back
  end
  <snip>
end

在我的控制器测试中,我想验证 (a) 正在设置 @real_time_device 并且 (b) #sync_reading 模型方法被调用(但我不想调用模型方法本身,因为那是模型单元测试涵盖)。

这是我的 controller_spec 代码不起作用:

# file: spec/controllers/real_time_devices_controller_spec.rb
require 'rails_helper'
  <snip>

    describe "PUT refresh" do
      it "assigns the requested real_time_device as @real_time_device" do
        real_time_device = RealTimeDevice.create! valid_attributes
        expect(real_time_device).to receive(:sync_readings)
        put :refresh, {:id => real_time_device.to_param}, valid_session
        expect(assigns(:real_time_device)).to eq(real_time_device)
      end
    end

  <snip>

当我运行测试时,实际的 RealTimeDevice#sync_readings 方法被调用,即它试图调用我模型中的代码。我想到了这条线:

        expect(real_time_device).to receive(:sync_readings)

对于存根方法并验证它是否被调用是必要且足够的。我的怀疑是它需要是双重的。但我也看不到如何使用双精度编写测试。

我错过了什么?

【问题讨论】:

    标签: ruby-on-rails unit-testing rspec controller


    【解决方案1】:

    您正在对RealTimeDevice 的特定实例设置期望。控制器从数据库中获取记录,但在您的控制器中,它使用RealTimeDevice 的另一个实例,而不是您设置期望的实际对象。

    这个问题有两种解决方案。

    又快又脏

    您可以对RealTimeDevice任何实例设置期望:

    expect_any_instance_of(RealTimeDevice).to receive(:sync_readings)
    

    请注意,这不是编写规范的最佳方式。毕竟,这并不能保证您的控制器从数据库中获取正确的记录。

    模拟方法

    第二种解决方案涉及更多工作,但会导致您的控制器被单独测试(如果它正在获取实际的数据库记录,则并非如此):

    describe 'PUT refresh' do
      let(:real_time_device) { instance_double(RealTimeDevice) }
    
      it 'assigns the requested real_time_device as @real_time_device' do
        expect(RealTimeDevice).to receive(:find).with('1').and_return(real_time_device)
        expect(real_time_device).to receive(:sync_readings)
    
        put :refresh, {:id => '1'}, valid_session
    
        expect(assigns(:real_time_device)).to eq(real_time_device)
      end
    end
    

    有些事情已经改变了。以下是发生的事情:

    let(:real_time_device) { instance_double(RealTimeDevice) }
    

    总是更喜欢在规范中使用let,而不是创建局部变量或实例变量。 let 允许您延迟评估对象,它不是在您的规范需要之前创建的。

    expect(RealTimeDevice).to receive(:find).with('1').and_return(real_time_device)
    

    数据库查找已被存根。我们告诉 rSpec 确保控制器从数据库中获取正确的记录。重要的部分是规范中创建的测试替身的实例正在此处返回。

    expect(real_time_device).to receive(:sync_readings)
    

    由于控制器现在使用的是测试替身而不是实际记录,因此您可以对测试替身本身设置期望。

    我使用了 rSpec 3 的instance_double,它验证了sync_readings 方法实际上是由底层类型实现的。这可以防止在缺少方法时通过规范。 Read more about verifying doubles in the rSpec documentation.

    请注意,完全不需要对实际的ActiveRecord 对象使用测试替身,但它确实使规范更快。控制器现在也在完全隔离的情况下进行测试。

    【讨论】:

    • 完美。现在我(更好地)理解了 instance_double() 的全部含义。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2011-02-17
    相关资源
    最近更新 更多