Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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.)

short form

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?

details

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?

like image 247
fearless_fool Avatar asked Dec 14 '22 18:12

fearless_fool


1 Answers

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.

The Quick and Dirty

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 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 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.

like image 200
fivedigit Avatar answered Dec 28 '22 07:12

fivedigit