在rspec中模拟控制器方法

时间:2014-11-26 07:38:08

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

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

简短形式

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

expect(real_time_device).to receive(:sync_readings)

验证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

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

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

我错过了什么?

1 个答案:

答案 0 :(得分:10)

您正在为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确保控制器从数据库中获取正确的记录。重要的是,在规范中创建的测试double的实例将在这里返回。

expect(real_time_device).to receive(:sync_readings)

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

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

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