Controller unit testing

I’m working through the Testing Fundamentals: Write a Controller Spec workshop and I’m having some difficulties getting my #create spec to pass using doubles for the model to be saved.

Spec

describe PeopleController do
  describe "#create" do
    it "redirects to #show" do
      fake_person = instance_double("Person", id: 1)
      allow(Person).to receive(:new).
        with(first_name: "John").
        and_return(fake_person)
      allow(fake_person).to receive(:save).and_return(true)

      post :create, person: { first_name: "John" }

      expect(response).to redirect_to person_path(1)
    end
  end

  ...
end

Controller

class PeopleController < ApplicationController
  ...  
  def create
    @person = Person.new(person_attributes)
    if @person.save
      redirect_to @person, notice: "Person created."
    else
      render :new
    end
  end
  ...
end   

I’m getting the following error when I run this spec:

1) PeopleController#create when person is valid redirects to #show
     Failure/Error: post :create, person: { first_name: "John" }
     NoMethodError:
       undefined method 'model_name' for RSpec::Mocks::InstanceVerifyingDouble:Class

I haven’t been able to Google my way out this this one so far, I have a feeling that when redirect_to is attempting to process my double something is failing. I also have a feeling I’m probably approaching this problem of using doubles incorrectly hence the resistance I’m encountering. Any thoughts or hints in an alternative direction would be greatly appreciated!

Thanks,

-Dan

1 Like

Hey Dan,

I would make your fake person a stubbed FactoryGirl object rather than an instance double. I think what’s happening in your test is that your fake_person doesn’t have the ActiveRecord/ActiveModel machinery necessary to produce a path when used as the target of a redirect.

I would have Person.new return something like this:

FactoryGirl.build_stubbed(:person)

and then stub #save on person so that your test doesn’t touch the database (and remains fast). Even though it doesn’t store a permanent record, stubbed FactoryGirl objects do have a (fake) ID that can be used to build a path, and that should get around your error.

1 Like

Thanks for the suggestion Geoff, leveraging FactoryGirl’s build_stubbed did indeed solve the issue I was running into. I’m still curious how I could have resolved this without adding FactoryGirl, perhaps digging into FG’s source will yield some clues.

Thanks again for the assistance!

-Dan

TL;DR: More work than you want to take on unless someone has a cool trick I’m missing.

Hey @hansondr, you can do this without using FactoryGirl, but as far as I can tell (I might be missing a simple trick) it requires using a more fleshed out class rather than an instance double, because .model_name is a class method, not an instance method.

When you call redirect_to(some_model), ActionController goes through some branching logic, because redirect_to and url_for can take a string, a hash, a record, or a symbol as arguments, and there’s different behavior for each case. In the record or model case, ActionController calls #model_name_from_record_or_class, which returns an instance of ActiveModel::Name. It then uses that name object’s route keys combined with a singular or plural inflection calculated earlier in the process to pump out a route that consists of the route key (so in your case it would be the plural of person, so /people, plus the result of the model’s #to_param call. You can try this on a Person record at the console like this:

ActionController::Base.new.model_name_from_record_or_class(Person.first)

And you’ll get an instance of ActiveModel::Name to play with.

If you haven’t overridden them, #to_param is just the record ID, and the route keys (#singular_route_key and #route_key for plural) are the model name lower-cased and underscored (and either singular or pluralized).

I did a little bit of source-diving to figure out how hard this would be, here’s one of the pages from my search:
http://apidock.com/rails/ActionDispatch/Routing/PolymorphicRoutes/build_named_route_call

3 Likes

I ran into this same problem tonight and ended up using factory_girl as well. Any idea on if there was a more ‘native’ implementation?

There is a little known method in RSpec called stub_model that will stub an active model instance, giving you model_name, etc. try it

1 Like

I ran into the same problem with instance_double. I found this post while searching for an idea on how to get around this. Thanks to everyone who’s participated in this thread; especially @geoffharcourt for the explanation of what’s happening under the hood – I found this extremely helpful in finding my solution.

I wanted to see if there was different way than adding FactoryGirl to solve this problem and I stumbled across rspec-activemodel-mocks which looks like it’s specifically designed to handle just this scenario.

The mockmodel method generates a test double that acts like an Active Model model. This is different from the stubmodel method which generates an instance of a real ActiveModel class.

The benefit of mockmodel over stubmodel is that its a true double, so the examples are not dependent on the behaviour (or mis-behaviour), or even the existence of any other code. If you're working on a controller spec and you need a model that doesn't exist, you can pass mock_model a string and the generated object will act as though its an instance of the class named by that string.

I added the following to the Gemfile

gem 'rspec-activemodel-mocks'

Then I was able to replace my equivalent of

fake_person = instance_double("Person", id: 1)

with something like

fake_person = mock_model('Person', id: 1)

Afterward, the controller spec was able to run and resolve the correct pathname for the redirect.

2 Likes

I feel somewhat silly but i am really struggling on this exercise…

Regardless of what parameters I pass in, I get “param is missing or the value is empty” for person, even though I’m passing in a first_name parameter.

Can you share the relevant parts of your code?

Don’t worry, I got it working. Finally!