theANDYM
theANDYM

Reputation: 504

Rspec + Devise + Factory Girl Testing with Associations

I'm pretty new to Rspec and am migrating a codebase from Rails 3.0.x to Rails 3.1.x and adding in testing at the same time. I was able to get basic controller tests working, but after starting to integrate Devise I've started to run into issues. Specifically, I have a Devise object called User that belongs to a Company, and the Company has many Communities. I am trying to write tests for the Communities controller, and when trying to reference by association ( for instance controller.current_user.communities) I'm not able to reference Factory objects I've created. When I try to test I get the following (or a similar) error:

No route matches {:id=>nil, :controller=>"communities", :action=>"edit"}

I'm sure I'm missing something basic related to Rspec/Factory_Girl, but any help would be much appreciated.


An example of the setup for testing the edit action for Communities is as follows:

/config/routes.rb

Units::Application.routes.draw do

  devise_for :users

  resources :companies
  resources :communities

  ...

  root :to => 'companies#index'

end

/app/models/user.rb

class User < ActiveRecord::Base
  belongs_to :company
  has_many :communities, :through => :company
  ...
end

/app/models/company.rb

class Company < ActiveRecord::Base
  has_many :users
  has_many :communities
  ...
end

/app/models/community.rb

class Community < ActiveRecord::Base
  belongs_to :company
  ...
end

/spec/controllers/communities_controller_spec.rb

require 'spec_helper'

describe CommunitiesController do
  render_views
  login_user
  ...
  context "GET #edit" do
    before(:each) do
      @company = controller.current_user.company
      @community = controller.current_user.communities.new[Factory.build(:community, :company => @company)]
      controller.current_user.company.communities.should_receive(:find).with(:company => @company)
    end

    it "should be successful" do
      get :edit, :id => @community
      response.should be_successful
    end

    it "should find a specific community" do
      get :edit, :id => @community
      assigns(:community).should eq(@community)
    end
  end
  ...
end

/app/controllers/communities_controller.rb

class CommunitiesController < ApplicationController
  ...
  def edit
    @community = current_user.communities.find(params[:id])
  end
  ...
end

/spec/support/controller_macros.rb

module ControllerMacros
  def login_user
    before(:each) do
      @request.env["devise.mapping"] = Devise.mappings[:user]
      company = Factory.create(:company)
      user = Factory.create(:user, :company => company)
      # user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the confirmable module
      sign_in user
    end
  end
end

Upvotes: 2

Views: 1824

Answers (1)

Ben Simpson
Ben Simpson

Reputation: 4049

The error: "No route matches {:id=>nil, :controller=>"communities", :action=>"edit"}" is a result of the line in your before(:each) block that reads:

@community = controller.current_user.communities.new[Factory.build(:community, :company => @company)]

Note that Factory.build is different than Factory.create. Build creates a new instance of the object, but doesn't actually save it to the database. Further, you are creating a new community instance with this new Factory instance, without saving it. I would suggest creating your community as follows:

   @community = Factory.create(:community, :company => @company)

You are later taking this @community variable, and passing it in your test to the "get" method:

get :edit, :id => @community

Because community isn't saved, it doesn't have an ID. It only exists in memory. Further, it would be more correct to send get the id of the @community object (after you have updated Factory.build to read Factory.create):

get :edit, :id => @community.id

Second, your association does not appear to be consistent with the logic in your controller on line:

controller.current_user.company.communities.should_receive(:find).with(:company => @company)

In your controller, you are finding the community by chaining through current_user.communities, not current_user.company.communities. I would recommend removing this test since you want to test the outcome, not the implementation. This makes the tests too brittle to code changes.

Last, I would like to mention that you have multiple assertions in your tests. You are asserting in your before(:each):

controller.current_user.company.communities.should_receive(:find).with(:company => @company)

This is being run in each of the nested tests below it, so that each test is checking for two conditions. This should be better isolated. I would propose moving this code to its own test block, and moving it out of the before method:

it "should find communities" do
    controller.current_user.company.communities.should_receive(:find).with(:company => @company)
end

However, as I mention above, I don't think this test is a great idea as it is too tightly coupled with the implementation of what you are testing.

Upvotes: 3

Related Questions