(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.)
I want to test my controller code separate from my model code. Shouldn't the rspec code:
expect(real_time_device).to receive(:sync_readings)
verify that RealTimeDevice#sync_readings
gets called, but inhibit the actual call?
My controller has a #refresh method that calls 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).
Here's my controller_spec code that doesn't work:
# 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, i.e., it's trying to call code in my model. 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
. 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.
There are two solutions to this problem.
You can set an expectation on any instance of 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 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
allows you to lazy evaluate the object, it's not created before your spec requires it.
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. The important part is that the very instance of the test double created in the spec is being returned here.
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. This prevents specs from passing when a method would be missing. Read more about verifying doubles in the rSpec documentation.
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. The controller is now also tested in complete isolation.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With