TDD on Rails #4: RSpec, Authlogic, Factory_girl and resource_controller


So well, I decided to use RSpec, Authlogic, Factory_Girl and resource_controller on my new application! But I’ve encountered some problems making all these working together. I mean I did not find any good tutorial showing how to do it step by step, so there it is!


The setup:

  • rails 2.3.2
  • rubygems 1.3.2
  • ruby 1.8.6
  • Mac OS X Leopard
  • Textmate
  • rspec 1.2.4
  • rspec-rails 1.2.4
  • authlogic 2.0.9
  • factory_girl 1.2.1
  • giraffesoft-resource_controller 0.6.1



Let’s install all the things we need:

1
2
3
rails thoughts
gem source -a http://gems.github.com
sudo gem install  thoughtbot-factory_girl authlogic rspec rspec-rails giraffesoft-resource_controller



Let’s configure this in our new app. The environment.rb should look like this:

1
2
3
4
5
config.gem "authlogic", :version => ">= 2.0.9"
config.gem "giraffesoft-resource_controller", :lib => "resource_controller",  :version => ">= 0.6.1", :source => "git://github.com/giraffesoft/resource_controller.git"
config.gem "rspec", :lib => false, :version => ">= 1.2.4"
config.gem "rspec-rails", :lib => false, :version => ">= 1.2.4"
config.gem "thoughtbot-factory_girl", :lib => "factory_girl", :version => ">= 1.2.1"


Make sure you put the authlogic dependency before the resource_controller dependency because if you don’t, you’ll end up with the following error every first access to the app:
You must activate the Authlogic::Session::Base.controller with a controller object before creating objects


Settin up Authlogic:

For the authlogic part, I’ll basically follow the official tutorial modifying a little so I get more generated testing code. Read the tutorial if you want to have more information about the following. I’ll put a #TDD on Rails comment on the like I changed from the official tutorial.

1
2
3
4
./script/generate session user_session
./script/generate rspec_model user #TDD on rails
./script/generate rspec_controller user_sessions #TDD on rails
./script/generate rspec_controller users #TDD on rails



Modify your user migration so it looks like this:

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
class CreateUsers < ActiveRecord::Migration
  def self.up
    create_table :users do |t|
      t.string    :login,               :null => false                # optional, you can use email instead, or both
      t.string    :email,               :null => false                # optional, you can use login instead, or both
      t.string    :crypted_password,    :null => false                # optional, see below
      t.string    :password_salt,       :null => false                # optional, but highly recommended
      t.string    :persistence_token,   :null => false                # required
      t.string    :single_access_token, :null => false                # optional, see Authlogic::Session::Params
      t.string    :perishable_token,    :null => false                # optional, see Authlogic::Session::Perishability

      # Magic columns, just like ActiveRecord's created_at and updated_at. These are automatically maintained by Authlogic if they are present.
      t.integer   :login_count,         :null => false, :default => 0 # optional, see Authlogic::Session::MagicColumns
      t.integer   :failed_login_count,  :null => false, :default => 0 # optional, see Authlogic::Session::MagicColumns
      t.datetime  :last_request_at                                    # optional, see Authlogic::Session::MagicColumns
      t.datetime  :current_login_at                                   # optional, see Authlogic::Session::MagicColumns
      t.datetime  :last_login_at                                      # optional, see Authlogic::Session::MagicColumns
      t.string    :current_login_ip                                   # optional, see Authlogic::Session::MagicColumns
      t.string    :last_login_ip                                      # optional, see Authlogic::Session::MagicColumns
     
      t.timestamps
    end
  end

  def self.down
    drop_table :users
  end
end



Modify your model:

1
2
3
4
5
class User < ActiveRecord::Base
  acts_as_authentic do |c|
    # c.my_config_option = my_value # for available options see documentation in: Authlogic::ActsAsAuthentic
  end # block optional
end



Modify the user_sessions_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
class UserSessionsController < ApplicationController
  before_filter :require_no_user, :only => [:new, :create]
  before_filter :require_user, :only => :destroy
 
  def new
    @user_session = UserSession.new
  end
 
  def create
    @user_session = UserSession.new(params[:user_session])
    if @user_session.save
      flash[:notice] = "Login successful!"
      redirect_back_or_default account_url
    else
      render :action => :new
    end
  end
 
  def destroy
    current_user_session.destroy
    flash[:notice] = "Logout successful!"
    redirect_back_or_default new_user_session_url
  end
end



Create a /app/views/user_sessions/new.html.erb:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<h1>Login</h1>
 
<% form_for @user_session, :url => user_session_path do |f| %>
  <%= f.error_messages %>
  <%= f.label :login %><br />
  <%= f.text_field :login %><br />
  <br />
  <%= f.label :password %><br />
  <%= f.password_field :password %><br />
  <br />
  <%= f.check_box :remember_me %><%= f.label :remember_me %><br />
  <br />
  <%= f.submit "Login" %>
<% end %>



Here is your users_controller:

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
class UsersController < ApplicationController
  before_filter :require_no_user, :only => [:new, :create]
  before_filter :require_user, :only => [:show, :edit, :update]
 
  def new
    @user = User.new
  end
 
  def create
    @user = User.new(params[:user])
    if @user.save
      flash[:notice] = "Account registered!"
      redirect_back_or_default account_url
    else
      render :action => :new
    end
  end
 
  def show
    @user = @current_user
  end
 
  def edit
    @user = @current_user
  end
 
  def update
    @user = @current_user # makes our views "cleaner" and more consistent
    if @user.update_attributes(params[:user])
      flash[:notice] = "Account updated!"
      redirect_to account_url
    else
      render :action => :edit
    end
  end
end


Grab the users_controller views from the official tutorial: users_controller view.


The application_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
# Filters added to this controller apply to all controllers in the application.
# Likewise, all the methods added will be available for all controllers.

class ApplicationController < ActionController::Base
  helper :all # include all helpers, all the time
  protect_from_forgery # See ActionController::RequestForgeryProtection for details
  helper_method :current_user_session, :current_user
  filter_parameter_logging :password, :password_confirmation
 
  private
    def current_user_session
      return @current_user_session if defined?(@current_user_session)
      @current_user_session = UserSession.find
    end
   
    def current_user
      return @current_user if defined?(@current_user)
      @current_user = current_user_session && current_user_session.record
    end
   
    def require_user
      unless current_user
        store_location
        flash[:notice] = "You must be logged in to access this page"
        redirect_to new_user_session_url
        return false
      end
    end
 
    def require_no_user
      if current_user
        store_location
        flash[:notice] = "You must be logged out to access this page"
        redirect_to account_url
        return false
      end
    end
   
    def store_location
      session[:return_to] = request.request_uri
    end
   
    def redirect_back_or_default(default)
      redirect_to(session[:return_to] || default)
      session[:return_to] = nil
    end
  # Scrub sensitive parameters from your log
  # filter_parameter_logging :password
end


Now let’s configure the routes.rb:

1
2
3
4
map.resource :user_session
map.resource :account, :controller => "users"
map.resources :users
map.root :controller => "user_sessions", :action => "new" # optional, this just sets the root route


That should do it.


Using RSepc:

Obviously we did not do much more than the official tutorial did in the first place… We just generated the test code using the rspec generating scripts. So let’s get started with the testing part! First let’s configure the Rspec part and see if we already some test failing:

1
2
3
./script/generate rspec
rake db:migrate RAILS_ENV=test
spec spec



Yeah! we already have all the rspec/controller/model testing stuff created. And we even have 1 of the 5 tests failing! And that’s our user model because it need to have the right attributes and does not (rspec model generation script create test for an empty model).

Make the test pass modifying the /spec/models/user_spec.rb file like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

describe User do
  before(:each) do
    @valid_attributes = {
      :login => "NewUser",
      :password => "12345678",
      :password_confirmation => "12345678",
      :email => "newuser@newuser.com",
    }
  end

  it "should create a new instance given valid attributes" do
    User.create!(@valid_attributes)
  end
end



Sweet! But we did not use the factory_girl fixtures replacement yet.


Factory_girl:


Let’s do: add require ‘factory_girl’ in your /spec/spec_helper.rb. Delete the /spec/fixtures folder. And create the following file: /spec/factories/users.rb

1
2
3
4
5
6
7
8
9
10
Factory.define :valid_user , :class => User do |u|
  u.login "mathieu"
  u.password "mathieu"
  u.password_confirmation "mathieu"
  u.email "mathieu.rousseau.31@gmail.com"
  u.single_access_token "k3cFzLIQnZ4MHRmJvJzg"
end

Factory.define :invalid_user , :class => User do |u|
end



Then create 2 new examples in your /spec/models/user_spec.rb file:

1
2
3
4
5
6
7
it "should succeed creating a new :valid_user from the Factory" do
  Factory.create(:valid_user)
end
 
it "should invalid :invalid_user factory" do
  Factory.build(:invalid_user).should be_invalid
end



Let’s see what we’ve done here: we created a new User object from the :valid_user object of the User factory and persisted it: Factory.create(:valid_user). And that’s just all we need to do as the create method automatically saves the user so that if it were invalid it would fail.
Then we built another User object from a invalid user from the factory: the build method does not save the User it does not fail building the user. And we make sure that the example tests if the created user is indeed invalid!


Simple isn’t it? So we tested the basic User part of the authlogic API but there is much more to do.


The rest of the app: resource_controller and testing authenticated controllers with authlogic and Rspec:

Let’s test the controller part, and that’s where I struggled a little. But before let’s create a little more stuff for our Thoughts app: Thought model and controller (using the resource_controller api) so it respect the following:
- a User has many thoughts
- a Thought belongs to 1 user
- You need to be authenticated to be able to create or delete thoughts

1
./script/generate scaffold_resource Thought title:string description:string user_id:integer


Add has_many :thoughts to the user.rb model and belongs_to :user to the thought.rb model.

The title, description and user_id of the thoughts table cannot be null:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CreateThoughts < ActiveRecord::Migration
  def self.up
    create_table :thoughts, :force => true do |t|
      t.string :title, :null => false
      t.string :description, :null => false
      t.integer :user_id, :null => false

      t.timestamps
    end
  end

  def self.down
    drop_table :thoughts
  end
end


And the Thought model should reflect that:

1
2
3
4
5
6
class Thought < ActiveRecord::Base
  belongs_to :user
  validates_presence_of :title, :on => :create, :message => "can't be blank"
  validates_presence_of :description, :on => :create, :message => "can't be blank"
  validates_presence_of :user, :on => :create, :message => "can't be blank"
end


Here is the thought migration:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CreateThoughts < ActiveRecord::Migration
  def self.up
    create_table :thoughts, :force => true do |t|
      t.string :title, :null => false
      t.string :description, :null => false
      t.integer :user_id, :null => false

      t.timestamps
    end
  end

  def self.down
    drop_table :thoughts
  end
end



Now you should end up with 54 examples which one of them should be failing: the Thought model spec is complaining that User, Title and description should not be blank. Let’s fix the /spec/models/thought_spec.rb spec and the thoughts.rb factory:

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
#/spec/factories/thoughts.rb
Factory.define :valid_thought, :class => Thought do |t|
  t.title "First thought"
  t.description "First thought description"
  t.user { |u| u.association(:valid_user) }
end

Factory.define :invalid_thought, :class => Thought do |t|
end
<!--more-->
<!--more-->
#/spec/models/thought_spec.rb
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

describe Thought do
  before(:each) do
    @thought = Factory.build(:valid_thought)
  end

  it "should be valid" do
    @thought.should be_valid
    @thought.save!
  end
 
  it "should be invalid" do
    Factory.build(:invalid_thought).should be_invalid
  end
end



Allright, now let’s see the thoughts_controller_spec.rb file: resource_controller created it and has not less than 28 examples. Nice but it all pass… and we should not be able to access the thought controller without being authenticated…

Let’s add the following to the /spec/controllers/thoughts_controller_spec.rb file:

1
2
3
4
5
6
describe "shouble be authenticated" do
  it "should fail if we are not authenticated" do
    get :index
    response.should_not be_success
  end
end


This test is failing as expected, let’s force the authentication on our thoughts_controller adding the following: before_filter :require_user.

Now the test is passing but 27 other tests are failing. And that’s just what we wanted to do: non-authenticated test should not pass. Few things needs to be done:

  • add require ‘authlogic/test_case’ in our spec_helper.rb file
  • active authlogic in our spec
  • create a valid session before executing out examples


To activate authlogic and create a new session for our test, we need to nest the resource_controller created tests inside the following:

1
2
3
4
5
6
7
8
9
describe "Authenticated examples" do
  before(:each) do
    activate_authlogic
    UserSession.create Factory.build(:valid_user)
  end

  #resource_controller generated tests

end



When I first tried to make this work, I tried to activate authlogic and create the session in a before(:all) block. But as the controller is instantiated only before each example, the before(:all) block has no control over the controller and authlogic can not be activated nor can we create a UserSession. So we need to do this in the before(:each) block and don’t forget not to include the example we coded into this description because it is testing the opposite: we should be authenticated to access the thought controller.


There is obviously much more work to be done on the “Thoughts” application: let one see only his thoughts and such. But I think I made my point and I’ll let you do the rest.


Problems:

I faced a very peculiar problem when the users in my User’s Factory were automatically created (and persisted) before running the test and made most of my tests fail… The only solution i found was to delete the /db/test.sqlite file.


I’d love to have some feedback and suggestion about this article! :-)


The application created here is available on my github account: http://github.com/anyware/thoughts/tree/master

To get the version as it is at the end of this post, just do:

1
2
git clone git://github.com/anyware/thoughts.git
git checkout 55fa648023d2f17d9dd7d5e76eee3e45d6e1c037

, , , , ,

  1. #1 by Ben Johnson on April 23rd, 2009

    Great post, I’m glad you took the time to share this, I’m sure it will be useful to a lot of people.

    Ben Johnson’s last blog post..Exclusive programming community?

  2. #2 by Tom Clark on April 30th, 2009

    I am currently doing EXACTLY this! Thanks for a great post!

  3. #3 by Lonnon Foster on April 30th, 2009

    Fantastic! I couldn’t figure out how to get a UserSession created, which was preventing me from spec’ing views that require a user to be logged in. Thanks for taking the time to post.

    Lonnon Foster’s last blog post..Fixing git pre-commit hook woes

  4. #4 by GlennR on May 7th, 2009

    Great post mate – exactly what I was looking for!

    GlennR’s last blog post..Defragment your day

  5. #5 by Bruno on May 8th, 2009

    Great post!

    Bruno’s last blog post..Fink: A "Cygwin" for Mac?

  6. #6 by Shane on May 20th, 2009

    Excellent post! Just started to look at authlogic.

    BTW, should ./script/generate scaffold_resource be ./script/generate rspec_scaffold for the Thoughts resource?

  7. #7 by Mathieu on May 22nd, 2009

    @Shane
    Thanks :-)

    ./script/generate scaffold_resource comes with resource_controller. Have a look at: http://github.com/giraffesoft/resource_controller/tree/master

  8. #8 by Shane on May 25th, 2009

    Ah … my bad! I was using just standard Rails resources. Thanks for that.

  9. #9 by Jesse on July 12th, 2009

    Hello, thanks so much for this. It helps me out a lot.
    Jesse´s last blog ..Open Hype? My ComLuv Profile

  10. #10 by Dalto Curvelano Júnior on September 4th, 2009

    Thank you! This was really helpfull!
    Dalto Curvelano Júnior´s last blog ..Zen of Python My ComLuv Profile

  11. #11 by xponrails on November 1st, 2009

    Great!

    A very nice tutorial, very useful.

    Stefano (http://xponrails.net)
    xponrails´s last blog ..How to implement a viewing system in Rails My ComLuv Profile

  12. #12 by Trevor Menagh on February 11th, 2010

    This tutorial has solved a LOT of my problems with getting rspec/factory_girl and authlogic playing nicely. You are a life-saver!

  13. #13 by Trevor Menagh on February 11th, 2010

    I think I might have solved your problem with users with persistence keys being created. I changed my user factory to use sequences instead of making a static user: http://gist.github.com/301719

  14. #14 by Prodis a.k.a. Fernando Hamasaki de Amorim on March 4th, 2010

    Thank you for tutorial.
    It helps me and my team to solve some problems.
    Prodis a.k.a. Fernando Hamasaki de Amorim´s last blog ..Métodos que retornam mais de um valor em Ruby My ComLuv Profile

(will not be published)
CommentLuv Enabled

  1. No trackbacks yet.