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 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 by Tom Clark on April 30th, 2009
I am currently doing EXACTLY this! Thanks for a great post!
#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 by GlennR on May 7th, 2009
Great post mate – exactly what I was looking for!
GlennR’s last blog post..Defragment your day
#5 by Bruno on May 8th, 2009
Great post!
Bruno’s last blog post..Fink: A "Cygwin" for Mac?
#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 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 by Shane on May 25th, 2009
Ah … my bad! I was using just standard Rails resources. Thanks for that.
#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?
#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
#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
#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 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 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