简体   繁体   English

在rspec中模拟控制器方法

[英]mocking controller methods in rspec

(This question is similar to Ruby on Rails Method Mocks in the Controller , but that was using the old stub syntax, and besides, that didn't receive a working answer.) (这个问题类似于Controller中的Ruby on Rails方法模拟 ,但这是使用旧的stub语法,而且,它没有收到有效的答案。)

short form 简写

I want to test my controller code separate from my model code. 我想测试我的控制器代码与我的模型代码分开。 Shouldn't the rspec code: 不应该是rspec代码:

expect(real_time_device).to receive(:sync_readings)

verify that RealTimeDevice#sync_readings gets called, but inhibit the actual call? 验证RealTimeDevice#sync_readings被调用,但是禁止实际调用?

details 细节

My controller has a #refresh method that calls 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

In my controller tests, I want to verify that (a) that @real_time_device is being set up and (b) the #sync_reading model method is getting called (but I don't want to invoke the model method itself since that's covered by the model unit tests). 在我的控制器测试中,我想验证(a)是否正在设置@real_time_device并且(b)调用#sync_reading模型方法(但我不想调用模型方法本身,因为它已被模型单元测试)。

Here's my controller_spec code that doesn't work: 这是我的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>

When I run the test, the actual RealTimeDevice#sync_readings method is getting called, ie, it's trying to call code in my model. 当我运行测试时,实际的RealTimeDevice#sync_readings方法被调用,即它试图在我的模型中调用代码。 I thought the line: 我认为这句话:

        expect(real_time_device).to receive(:sync_readings)

was necessary and sufficient to stub the method and verify that it got called. 是必要的,足以将方法存根并验证它被调用。 My suspicion is that it needs to be a double. 我怀疑它需要是双倍的。 But I can't see how to write the test using a double either. 但我无法看到如何使用双倍编写测试。

What am I missing? 我错过了什么?

You're setting an expectation on a specific instance of RealTimeDevice . 您正在为RealTimeDevice的特定实例设置期望。 The controller fetches the record from the database, but in your controller, it's using another instance of RealTimeDevice , not the actual object you set the expectation on. 控制器从数据库中获取记录,但在您的控制器中,它使用的是RealTimeDevice的另一个实例,而不是您设置期望的实际对象。

There are two solutions to this problem. 这个问题有两种解决方案。

The Quick and Dirty 快速和肮脏

You can set an expectation on any instance of RealTimeDevice : 您可以在RealTimeDevice 任何实例上设置期望:

expect_any_instance_of(RealTimeDevice).to receive(:sync_readings)

Note that this is not the best way to write your spec. 请注意,这不是编写规范的最佳方式。 After all, this doesn't guarantee that your controller fetches the right record from the database. 毕竟,这并不能保证您的控制器从数据库中获取正确的记录。

The Mocking Approach 嘲弄方法

The second solution involves a bit more work, but will cause your controller to be tested in isolation (which it is not really if it's fetching actual database records): 第二个解决方案涉及更多的工作,但会导致您的控制器被隔离测试(如果它正在获取实际的数据库记录,它实际上并不是这样):

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

Quite some things have changed. 有些事情发生了变化。 Here's what happens: 这是发生的事情:

let(:real_time_device) { instance_double(RealTimeDevice) }

Always prefer using let in your specs rather than creating local variables or instance variables. 总是更喜欢在规范中使用let而不是创建局部变量或实例变量。 let allows you to lazy evaluate the object, it's not created before your spec requires it. let允许你懒惰地评估对象,它不是在你的规范要求之前创建的。

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

The database lookup has been stubbed. 数据库查找已存根。 We're telling rSpec to make sure that the controller fetches the correct record from the database. 我们告诉rSpec确保控制器从数据库中获取正确的记录。 The important part is that the very instance of the test double created in the spec is being returned here. 重要的是,在规范中创建的测试double的实例将在此处返回。

expect(real_time_device).to receive(:sync_readings)

Since the controller is now using the test double rather than an actual record, you can set expectations on the test double itself. 由于控制器现在使用的是测试双精度而不是实际记录,因此您可以设置测试双精度本身的期望值。

I've used rSpec 3's instance_double , which verifies the sync_readings method is actually implemented by the underlying type. 我使用了rSpec 3的instance_double ,它验证了sync_readings方法实际上是由底层类型实现的。 This prevents specs from passing when a method would be missing. 这可以防止在缺少方法时传递规范。 Read more about verifying doubles in the rSpec documentation . 阅读有关在rSpec文档中验证双精度的更多信息

Note that it's not required at all to use a test double over an actual ActiveRecord object, but it does make the spec much faster. 请注意,完全不需要在实际的ActiveRecord对象上使用测试双ActiveRecord ,但它确实使规范更快。 The controller is now also tested in complete isolation. 控制器现在也经过完全隔离测试。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM