在rspec中模拟控制器方法

fea*_*ool 3 unit-testing controller rspec ruby-on-rails

(这个问题与Controller中的Ruby on Rails方法模拟类似,但是它使用的是旧stub语法,而且没有得到正确的答案.)

简写

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

expect(real_time_device).to receive(:sync_readings)
Run Code Online (Sandbox Code Playgroud)

验证是否RealTimeDevice#sync_readings被调用,但是禁止实际调用?

细节

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

# 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
Run Code Online (Sandbox Code Playgroud)

在我的控制器测试中,我想验证(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>
Run Code Online (Sandbox Code Playgroud)

当我运行测试时,实际的RealTimeDevice#sync_readings方法被调用,即它试图在我的模型中调用代码.我认为这句话:

        expect(real_time_device).to receive(:sync_readings)
Run Code Online (Sandbox Code Playgroud)

是必要的,足以将方法存根并验证它被调用.我怀疑它需要是双倍的.但我无法看到如何使用双倍编写测试.

我错过了什么?

fiv*_*git 10

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

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

快速和肮脏

您可以在以下任何实例上设置期望RealTimeDevice:

expect_any_instance_of(RealTimeDevice).to receive(:sync_readings)
Run Code Online (Sandbox Code Playgroud)

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

嘲弄方法

第二个解决方案涉及更多工作,但会导致您的控制器被隔离测试(如果它正在获取实际的数据库记录,则不是这样):

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
Run Code Online (Sandbox Code Playgroud)

有些事情发生了变化.这是发生的事情:

let(:real_time_device) { instance_double(RealTimeDevice) }
Run Code Online (Sandbox Code Playgroud)

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

expect(RealTimeDevice).to receive(:find).with('1').and_return(real_time_device)
Run Code Online (Sandbox Code Playgroud)

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

expect(real_time_device).to receive(:sync_readings)
Run Code Online (Sandbox Code Playgroud)

由于控制器现在使用的是测试双精度而不是实际记录,因此您可以设置测试双精度本身的期望值.

我使用了rSpec 3 instance_double,它验证了该sync_readings方法实际上是由底层类型实现的.这可以防止在缺少方法时传递规范.阅读有关在rSpec文档中验证双精度的更多信息.

请注意,完全不需要在实际ActiveRecord对象上使用测试双精度,但它确实使规范更快.控制器现在也经过完全隔离测试.