I've been learning Rails 3 with Devise and, so far, seem to have it working quite well. I've got custom session & registration controllers, recaptcha is working and a signed-in user can upload an avatar via carrierwave, which is saved on S3. Pretty happy with my progress.
Now I'm writing Rspec tests. Not going so well! I have a reasonable User model test, but that's because I found it online (https://github.com/RailsApps/rails3-devise-rspec-cucumber/) and was able to add to it by following Michael Hartl's excellent "Ruby on Rails 3 Tutorial".
My real problem is controller test and integration tests, especially controller tests. Initially I thought I'd be able to convert the tests in Michael's book, and I have to a small degree, but it's slow progress and I seem to be constantly hitting my head against a brick wall - partly, I think, because I don't know Rspec and capybara so well (have made some very dumb mistakes) but also because I don't really understand Devise well enough and am wondering if Devise plays as nicely as it might with Rspec; I read somewhere that, because Devise is Rack based, it might not always work as one might expect with Rspec. Don't know if that's true or not?
I know some people will wonder why this might be necessary since Devise is a gem and therefore already tested but I've had a couple of instances where changes elsewhere have broken login or registration without me immediately realizing. I think a good set of controller & integration tests would have solved this.
If I was able to do this myself I would and I'd publish it for others but, so far, writing these tests has been extremely painful and I really need to move on to other things.
I'm sure I wouldn't be the only one who could use this. Anyone know of a such a suite of tests?
In response to Jesse's kind offer of help...
Here is my registrations_controller_spec. The comments in "should render the 'edit' page" show the sort of things I am struggling with. Also, "should create a user" has some things I've tried to test but not been able to:
require File.dirname(__FILE__) + '/../spec_helper'
describe Users::RegistrationsController do
include Devise::TestHelpers
fixtures :all
render_views
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
end
describe "POST 'create'" do
describe "failure" do
before(:each) do
#attr = { :email => "", :password => "",
:password_confirmation => "", :display_name => "" }
end
it "should not create a user" do
lambda do
post :create, :user_registration => #attr
end.should_not change(User, :count)
end
it "should render the 'new' page" do
post :create, :user_registration => #attr
response.should render_template('new')
end
end
describe "success" do
before(:each) do
#attr = { :email => "user#example.com",
:password => "foobar01", :password_confirmation => "foobar01", :display_name => "New User" }
end
it "should create a user" do
lambda do
post :create, :user => #attr
response.should redirect_to(root_path)
#response.body.should have_selector('h1', :text => "Sample App")
#response.should have_css('h1', :text => "Sample App")
#flash[:success].should == "A message with a confirmation link has been sent to your email address. Please open the link to activate your account."
#response.should have_content "A message with a confirmation link has been sent to your email address. Please open the link to activate your account."
end.should change(User, :count).by(1)
end
end
end
describe "PUT 'update'" do
before(:each) do
#user = FactoryGirl.create(:user)
#user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the confirmable module
sign_in #user
end
describe "Failure" do
before(:each) do
# The following information is valid except for display_name which is too long (max 20 characters)
#attr = { :email => #user.email, :display_name => "Test", :current_password => #user.password }
end
it "should render the 'edit' page" do
put :update, :id => subject.current_user, :user => #attr
# HAVE PUT THE DEBUGS THAT I'D LIKE TO GET WORKING FIRST
# Would like to be able to debug and check I'm getting the error(s) I'm expecting
puts subject.current_user.errors.messages # doesn't show me the errors
# Would like to be able to debug what html is being returned:
puts page.html # only return the first line of html
# Would like to be able to determine that this test is failing for the right reasons
response.should have_content "Display name is too long (maximum is 20 characters)" # doesn't work
response.should render_template('edit')
end
end
describe "Success" do
it "should change the user's display name" do
#attr = { :email => #user.email, :display_name => "Test", :current_password => #user.password }
put :update, :id => subject.current_user, :user => #attr
subject.current_user.reload
response.should redirect_to(root_path)
subject.current_user.display_name == #attr[:display_name]
end
end
end
describe "authentication of edit/update pages" do
describe "for non-signed-in users" do
before(:each) do
#user = FactoryGirl.create(:user)
end
describe "for non-signed-in users" do
it "should deny access to 'edit'" do
get :edit, :id => #user
response.should redirect_to(new_user_session_path)
end
it "should deny access to 'update'" do
put :update, :id => #user, :user => {}
response.should redirect_to(new_user_session_path)
end
end
end
end
end
Acording to Devise Wiki, controller tests have to be some kind of hibrid (unit + integration) tests. You have to create an instance of User (or you auth entity) in the database. Mocking Devise stuff is really hard, trust me.
Try this: How-To:-Controllers-and-Views-tests-with-Rails-3-and-rspec
With Cucumber, you can create a step that already do the login process.
Hope it helps.
OK, so a couple of things to make this work:
Here's how you should test that the body have specific text (you were missing the .body
response.body.should include "Display name is too long (maximum is 30 characters)"
You could also test that the #user has an error:
assigns[:user].errors[:display_name].should include "is too long (maximum is 30 characters)"
But, you do need to actually make the name be over 20/30 characters long, so I changed the attributes being posted to:
#attr = { :email => #user.email, :display_name => ("t" * 35), :current_password => #user.password }
https://gist.github.com/64151078628663aa7577
Related
Where should I test authorization with RSpec?
When you create a Rails application with RSpec, there are three folders that seems to be adequate:
spec/routing
spec/requests
spec/controllers
In which one should I test if the user is logged in? Should I test in more than one spec type?
There is a subtle distinction to your question. Authorization usually referrers to permissions that users have within the app. Authentication referrers to users signing up and logging in users.
As far as Authentication goes, I usually prefer to use integration/requests specs or acceptance/feature specs. Feature specs are preferred lately because the Capybara DSL (page and visit) are only available in feature specs. They used to be allowed in request specs until the 2.x upgrade.
I will test things like signing up, signing in and signing out. For example,
# signing_up_spec.rb
feature 'Signing up' do
scenario 'Successful sign up' do
visit '/'
within 'nav' do
click_link 'Sign up'
end
fill_in "Email", :with => "user#ticketee.com"
fill_in "Password", :with => "password"
fill_in "Password confirmation", :with => "password"
click_button "Sign up"
page.should have_content("Please open the link to activate your account.")
end
end
This allows you to test the higher level aspect and lets you see the different components (controllers, views, etc...) within your app working together. This is by definition an integration/acceptance test. I would do the same as above for signing_in_spec.rb and signing_out_spec.rb
Now for Authorization, I would choose to use controller specs. This allows you to test the individual actions a user has permission to access. These controller specs are more granular in nature and are by definition unit/function tests. For example, say you had a ticket resource and you want to test that only certain users can access some particular functionality
# tickets_controller_spec.rb
describe TicketsController do
let(:user) { FactoryGirl.create(:confirmed_user) }
let(:project) { FactoryGirl.create(:project) }
let(:ticket) { FactoryGirl.create(:ticket, :project => project,
:user => user) }
context "standard users" do
it "cannot access a ticket for a project" do
sign_in(:user, user)
get :show, :id => ticket.id, :project_id => project.id
response.should redirect_to(root_path)
flash[:alert].should eql("The project you were looking for could not be found.")
end
context "with permission to view the project" do
before do
sign_in(:user, user)
define_permission!(user, "view", project)
end
def cannot_create_tickets!
response.should redirect_to(project)
flash[:alert].should eql("You cannot create tickets on this project.")
end
def cannot_update_tickets!
response.should redirect_to(project)
flash[:alert].should eql("You cannot edit tickets on this project.")
end
it "cannot begin to create a ticket" do
get :new, :project_id => project.id
cannot_create_tickets!
end
it "cannot create a ticket without permission" do
post :create, :project_id => project.id
cannot_create_tickets!
end
it "cannot edit a ticket without permission" do
get :edit, { :project_id => project.id, :id => ticket.id }
cannot_update_tickets!
end
it "cannot update a ticket without permission" do
put :update, { :project_id => project.id,
:id => ticket.id,
:ticket => {}
}
cannot_update_tickets!
end
it "cannot delete a ticket without permission" do
delete :destroy, { :project_id => project.id, :id => ticket.id }
response.should redirect_to(project)
flash[:alert].should eql("You cannot delete tickets from this project.")
end
it "can create tickets, but not tag them" do
Permission.create(:user => user, :thing => project, :action => "create tickets")
post :create, :ticket => { :title => "New ticket!",
:description => "Brand spankin' new",
:tag_names => "these are tags"
},
:project_id => project.id
Ticket.last.tags.should be_empty
end
end
end
end
I have found that the combination of rspec-rails, capybara and factory_girl_rails have served well in both types of testing within rails apps.
The examples above were taken from the Rails3Book repo on github. Take a look at the repo for more examples. It is a great way to see the possibilities you have when testing a rails app.
Rails noob here. I'm having trouble understanding how/what to test for regarding authentication with Omniauth-Facebook. Pretty much have a similar setup to the relevant railscast. I have my test environment set up similarly to what's described in previous questions and on the Gem wiki.
Couple of questions. When you create a User factory how do you get resulting object to mock an authentication?
Also what goes on when the following code is run?
before do
request.env["omniauth.auth"] = OmniAuth.config.mock_auth[:facebook]
visit '/auth/facebook'
end
Is an object saved in a database?
I've added some sample specs below and the Factory spec. Again the spec_helper setup file has the test mode config set to true.
The add_mock setup:
OmniAuth.config.add_mock(:facebook,
{ :provider => 'facebook',
:uid => '1234567',
:info => { :name => 'Jonathan', :image => 'http://graph.facebook.com/1234567/picture?type=square'},
:credentials => {
:expires_at => 1351270850,
:token=> 'AAADzk0b791YBAHCNhBI3n6ScmWvuXTY4yIUqXr9WiZCg1R808RYzaHxsHnrbn62IwrIgZCfSBZAVIP6ptF41nm8YtRtZCeBbxbbz1mF8RQZDZD'
} })
User_pages_spec:
describe "user pages" do
let(:user) { Factory.create(:user) }
describe "sign_in" do
before { visit '/' }
it "should add a user to the User model" do
expect { click_link "sign_in" }.to change(User, :count).by(1)
end
it "should route to the appropriate page if email is nil" do
click_link "sign_in"
page.should have_selector('h5', text: 'Edit Email')
end
it "should redirect to the show page upon" do
user.email = 'example#stanford.edu'
user.save!
click_link 'sign_in'
page.should have_selector('div', text: user.name)
end
end
end
Factory
FactoryGirl.define do
factory :user do
provider "facebook"
uid '1234567'
name 'Jonathan'
end
end
I have the following route:
devise_for :users, :controllers => { :omniauth_callbacks => "users/omniauth_callbacks",
:registrations => 'users/registrations',
:sessions => "users/sessions" }
and the following controller test (registrations_controller_spec.rb):
require File.dirname(__FILE__) + '/../spec_helper'
describe Users::RegistrationsController do
include Devise::TestHelpers
fixtures :all
render_views
before(:each) do
#request.env["devise.mapping"] = Devise.mappings[:user]
end
describe "POST 'create'" do
describe "success" do
before(:each) do
#attr = { :email => "user#example.com",
:password => "foobar01", :password_confirmation => "foobar01", :display_name => "New User" }
end
it "should create a user" do
lambda do
post :create, :user => #attr
response.should redirect_to(root_path)
end.should change(User, :count).by(1)
end
end
end
describe "PUT 'update'" do
before(:each) do
#user = FactoryGirl.create(:user)
#user.confirm! # or set a confirmed_at inside the factory. Only necessary if you are using the confirmable module
sign_in #user
end
describe "Success" do
it "should change the user's display name" do
#attr = { :email => #user.email, :display_name => "Test", :current_password => #user.password }
put :update, :id => #user, :user => #attr
puts #user.errors.messages
#user.display_name.should == #attr[:display_name]
end
end
end
end
Now, when I run rspec spec I get (what I think) are weird results:
The "should create a user" test passes. The user count has increased by 1.
However, my "should change the user's display name" fails as follows:
1) Users::RegistrationsController PUT 'update' Success should change the user's display name
Failure/Error: #user.display_name.should == #attr[:display_name]
expected: "Test"
got: "Boyd" (using ==)
And the weird bit is that my statement:
puts #user.errors.messages
Renders the following message:
{:email=>["was already confirmed, please try signing in"]}
What's going on? The user is signed in! That's proved by the fact that the Rspec error returned the display_name of "Boyd". And why is it displaying a message looks as though it's related with account confirmation as opposed to updating a user's details?
Any help would be greatly appreciated!
This works. Thanks holtkampw for seeing what I wasn't! I put some extra code in there just to double check and everything is well!
it "should change the user's display name" do
subject.current_user.should_not be_nil
#attr = { :email => #user.email, :display_name => "Test", :current_password => #user.password }
puts "Old display name: " + subject.current_user.display_name
put :update, :id => subject.current_user, :user => #attr
subject.current_user.reload
response.should redirect_to(root_path)
subject.current_user.display_name == #attr[:display_name]
puts "New display name: " + subject.current_user.display_name
end
I have been writing some rspec tests, and right now I want to verify a redirect after user creation. Many other tests work, including creating users.
However, the last of the following tests fail:
describe "success" do
before(:each) do
#attr = { :username => "woweee123",
:email => "email#mail.com",
:password => "password123",
:password_confirmation => "password123"}
end
it "should create the user" do
lambda do
post :create, :user => #attr
end.should change(User, :count)
end
it "should redirect to a user show page after creation" do
lambda do
post :create, :user => #attr
end.should redirect_to(user_path(assigns(:user)))
end
end
The failed test is: No route matches {:action=>"show", :controller=>"users"
But, when I create a user manually (on localhost) the redirect obviously works. Why is this failing?
I would check if
assigns(#user).new_record?
is not true anymore after the post. It seems like it doesn't have an id in your test. Could some validation have failed?
For Those interested the answer is that I had to change the test so that it looks like:
it "should redirect to a user show page after creation" do
post :create, :user => #attr
response.should redirect_to(user_path(assigns(:user)))
end
This works.
I've got a problem with my controller tests for my Courses Controller. It seems that devise is not signing in my user correctly. All the generated controller tests for this controller only are failing.
I create my user in users.rb using Factory-girl as per below...
FactoryGirl.define do
factory :user do
sequence :email do |n|
"test#{n}#email.com"
end
password "password"
password_confirmation "password"
end
end
Then in my courses_controller_spec.rb I simulate the login as per below..
require 'spec_helper'
describe CoursesController do
include Devise::TestHelpers
before(:each) do
##request.env["devise.mapping"] = Devise.mappings[:user]
user = Factory.create(:user)
user.toggle!(:admin)
sign_in user
end
describe "DELETE destroy" do
it "redirects to the courses list" do
course = Factory.create(:course)
delete :destroy, {:id => course.to_param}, valid_session
response.should redirect_to(courses_url)
end
end
And I get the output...
Failure/Error: response.should redirect_to(courses_url)
Expected response to be a redirect to <http://test.host/courses> but was a redirect to <http://test.host/users/sign_in>
Please note I've also used the following in my spec_helper.rb
config.include Devise::TestHelpers, :type => :controller
And I've tried it as per https://github.com/plataformatec/devise/wiki/How-To:-Controllers-and-Views-tests-with-Rails-3-%28and-rspec%29
In my request specs I can create the user and login using the below which works fine but I'd like to get all the controller tests working also
fill_in "Email", :with => user.email
fill_in "Password", :with => user.password
click_button "Sign in"
Any help here would be much appreciated.
I just figured this out myself. Remove the valid_session from your delete call. That seems to overwrite the session defined using the devise test helpers.
Everything else seems correct. Keep all the devise setup code and just change the delete line in your spec to:
delete :destroy, {:id => course.to_param}
I also kept in the following line in my before( :each ) block, which you commented out. Not sure what it does yet:
#request.env["devise.mapping"] = Devise.mappings[:user]