(此问题类似于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)
是必要的,足以将方法存根并验证它是否被调用。我怀疑它需要是双倍的。但我无法看到如何使用双倍编写测试。
我错过了什么?
答案 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
对象使用测试双精度完全不需要,但它确实使规范更快。现在,控制器也完全隔离了。