Rails 5 Rspec receive with ActionController::Params
Asked Answered
O

3

15

I have just upgraded to Rails 5. In my specs I have the following

expect(model).to receive(:update).with(foo: 'bar')

But, since params no longer extends Hash but is now ActionController::Parameters the specs are failing because with() is expecting a hash but it is actually ActionController::Parameters

Is there a better way of doing the same thing in Rspec such as a different method with_hash?

I can get around the issue using

expect(model).to receive(:update).with(hash_including(foo: 'bar'))

But that is just checking if the params includes that hash, not checking for an exact match.

Obrian answered 26/9, 2016 at 12:33 Comment(0)
S
15

You could do:

params = ActionController::Parameters.new(foo: 'bar')
expect(model).to receive(:update).with(params)

However it still smells - you should be testing the behaviour of the application - not how it does its job.

expect {
  patch model_path(model), params: { foo: 'bar' }
  model.reload
}.to change(model, :foo).to('bar')

This is how I would test the integration of a controller:

require 'rails_helper'
RSpec.describe "Things", type: :request do
  describe "PATCH /things/:id" do

    let!(:thing) { create(:thing) }
    let(:action) do
      patch things_path(thing), params: { thing: attributes }
    end

    context "with invalid params" do
      let(:attributes) { { name: '' } }
      it "does not alter the thing" do
         expect do 
           action 
           thing.reload
         end.to_not change(thing, :name)
         expect(response).to have_status :bad_entity
      end
    end

    context "with valid params" do
      let(:attributes) { { name: 'Foo' } }
       it "updates the thing" do
         expect do 
           action 
           thing.reload
         end.to change(thing, :name).to('Foo')
         expect(response).to be_successful
      end
    end
  end
end

Is touching the database in a spec inheritenly bad?

No. When you are testing something like a controller the most accurate way to test it is by driving the full stack. If we in this case had stubbed out @thing.update we could have missed for example that the database driver threw an error because we where using the wrong SQL syntax.

If you are for example testing scopes on a model then a spec that stubs out the DB will give you little to no value.

Stubbing may give you a fast test suite that is extremely brittle due to tight coupling and that lets plenty of bugs slip through the cracks.

Sweeney answered 26/9, 2016 at 12:53 Comment(7)
I've always thought that hitting the database within specs was a bad thing to do? That's why I always stub the method on the model, then return a double if I need to check for valid? and such. That way I can expect the double to receive valid? and return true or false based on what I want to test (success or failure)Obrian
Thats kind of an exaggeration - stubbing out the database can be a good idea in unit tests to make your test suite faster. But when you are testing something like a controller what you really are doing is functional or integration testing and all that stubbing ruins the acuity of your tests.Sweeney
DHH wrote a pretty interesting blog post on the subject a while back that managed to tickle a whole lot of people the wrong way.Sweeney
@Sweeney I won't recommend anyone who wants to became a good developer to follow any of dhh's articles or his codeFallon
No rails for you then @RustamA.Gasanov.Sweeney
I agree with not stubbing out models. But If I need to test a (service) object that is called by the controller, and depending of the input does a variety of things, it is so much easier to test that object separately and in the controller only test that it passes everything it needs to that object. Feature specs should than usually test that everything also integrated nicely in a basic scenario.Decorative
@Decorative thats a different scenario than your typical controller (and the original question) and more akin to when you're dealing with something like an API client that goes outside the application boundy. Yeah I would agree thats a place where stubbing could be useful.Sweeney
V
13

I handled this by creating in spec/rails_helper.rb

def strong_params(wimpy_params)
  ActionController::Parameters.new(wimpy_params).permit!
end

and then in a specific test, you can say:

expect(model).to receive(:update).with(strong_params foo: 'bar')

It's not much different from what you're already doing, but it makes the awkward necessity of that extra call a little more semantically meaningful.

Vassell answered 2/8, 2017 at 18:22 Comment(0)
B
2

@max had good suggestions about how to avoid this altogether, and I agree they switched away from a hash to discourage using them with hashes interchangeably.

However, if you still want to use them, as a simple hack for more complex situations (for instance if you expect using a a_hash_including), you can try using something like this:

  .with( an_object_satisfying { |o| 
           o.slice(some_params) == ActionController::Parameters.new(some_params)
         })
Burck answered 19/10, 2016 at 17:54 Comment(4)
Thanks for the answer, I implemented a hack around it in the specs while I decide on a better way. I basically added a to_params method which calls ActionController::Parameters.new hash so that it converts the hash in specs to ActionController::Params expect(foo).to receive(:bar).with(to_params(foo: 'bar'))Obrian
you monkey-patched a to_params method to ActionController::Parameters ?Burck
In the example in the comment, foo: 'bar' is converted to an instance of ActionController::ParametersObrian
Ahh ok. Yes, can easily do that. I used my above technique because I was using a_hash_including which won't allow for that kind of simple conversion. None of these are particularly ideal.Burck

© 2022 - 2024 — McMap. All rights reserved.