Closet Coder

I work in my closet. I code. Yep.

Rails Controller Specs With Users, Roles and Nested Routes

| Comments

I’ve long put off testing my controllers because of user authentication and nested controllers, dealing with stubs, etc.

But today, a fully working test!

As background, Advertisers have many trackers and the routes look like this:

config/routes.rb
1
2
3
4
5
ActionController::Routing::Routes.draw do |map|
  map.resources :advertisers do |advertisers|
    advertisers.resources :trackers
  end
end

To set everything up in the specs, I included all the files in the spec/support directory and used Mocha as my mock framework

spec/spec_helper.rb
1
2
3
4
5
Dir[File.expand_path(File.join(File.dirname(__FILE__),'support','**','*.rb'))].each {|f| require f}

Spec::Runner.configure do |config|
  config.mock_with :mocha
end

Then I set up my factories (rather than fixtures) using Factory Girl

spec/factories.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
Factory.define :user do |user|
  user.sequence(:login) { |n| "username#{n}" }
  user.password 'password'
  user.password_confirmation { |u| u.password }
  user.sequence(:email) { |n| "email#{n}@example.com" }
  user.first_name "Mama"
  user.last_name "Foo"
end

Factory.define :advertiser do |advertiser|
  advertiser.name 'Advertiser 1'
end

Factory.define :tracker do |tracker|
  tracker.name 'Tracker 1'
end

Now we get down to brass tacks. In order to make my tests DRY (appropriately) and allow for all my controllers to test if someone is logged in and has access, I set up this shared context

spec/support/user_authentication.rb
1
2
3
4
5
6
describe "an admin is logged in", :shared => true do
  before(:each) do
    controller.stubs( :login_required => true)
    controller.stubs( :current_user => Factory.build(:user, :login => 'admin', :roles_list => ["super"]))
  end
end

From there, all we need to do is put it all together, setting up trackers parent @advertiser and the @tracker we’ll be using and stubbing the ActiveRecord find so that it always returns @advertiser

spec/controllers/trackers_controller.rb
1
2
3
4
5
6
7
8
9
describe TrackersController do
  it_should_behave_like "an admin is logged in"
  integrate_views

  before(:each) do
    @advertiser = Factory.create(:advertiser)
    @tracker = @advertiser.trackers.create(Factory.attributes_for(:tracker))
    Advertiser.stubs(:find => @advertiser)
  end

Now we simply specify the part of the path that is needed to find the nested route by using :advertiser_id => @advertiser

spec/controllers/trackers_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
  it "index action should render index template" do
    get :index, :advertiser_id => @advertiser
    response.should render_template(:index)
  end

  it "show action should render show template" do
    get :show, :advertiser_id => @advertiser, :id => @tracker
    response.should render_template(:show)
  end

  it "new action should render new template" do
    get :new, :advertiser_id => @advertiser
    response.should render_template(:new)
  end

  it "create action should render new template when model is invalid" do
    Tracker.any_instance.stubs(:valid?).returns(false)
    post :create, :advertiser_id => @advertiser
    response.should render_template(:new)
  end

  it "create action should redirect when model is valid" do
    Tracker.any_instance.stubs(:valid?).returns(true)
    post :create, :advertiser_id => @advertiser
    response.should redirect_to(advertiser_tracker_url(@advertiser, assigns[:tracker]))
  end

  it "edit action should render edit template" do
    get :edit, :advertiser_id => @advertiser, :id => @tracker
    response.should render_template(:edit)
  end

  it "update action should render edit template when model is invalid" do
    Tracker.any_instance.stubs(:valid?).returns(false)
    put :update, :advertiser_id => @advertiser, :id => @tracker
    response.should render_template(:edit)
  end

  it "update action should redirect when model is valid" do
    Tracker.any_instance.stubs(:valid?).returns(true)
    put :update, :advertiser_id => @advertiser, :id => @tracker
    response.should redirect_to(advertiser_tracker_url(@advertiser, assigns[:tracker]))
  end

  it "destroy action should destroy model and redirect to index action" do
    delete :destroy, :advertiser_id => @advertiser, :id => @tracker
    response.should redirect_to(advertiser_trackers_url(@advertiser))
    Tracker.exists?(@tracker.id).should be_false
  end
end

Works! And works great! A minimal and excellent way to test your controllers, especially for access. You can easily create additional shared contexts with different user permissions and extend out the tests to make sure users that don’t have access can properly access them.