Friday, July 17, 2009

Rails: RSpec, Cucumber, Authlogic, and factory_girl

After a day of work and a week of reading The RSpec Book, I got RSpec, Cucumber, Authlogic, and factory_girl to play nicely with each other. I can now test user registration, logging in, and logging out.

I can't possibly cover everything from scratch, but I'll cover some of the trickier integration points.

Setup Rails, RSpec, Cucumber, and Authlogic per their own documentation. I ended up with the following in both config/environments/test.rb and config/environments/cucumber.rb:
# There is some duplication between test.rb and cucumber.rb.
config.gem "cucumber", :lib => false, :version => ">=0.3.11"
config.gem "webrat", :lib => false, :version => ">=0.4.4"
config.gem "rspec", :lib => false, :version => ">=1.2.6"
config.gem "rspec-rails", :lib => 'spec/rails', :version => ">=1.2.6"
config.gem "thoughtbot-factory_girl", :lib => "factory_girl", :version => ">=1.2.2", :source => "http://gems.github.com"
I created a factory in test/factories/user.rb:
Factory.define :user do |u|
u.username 'john'
u.email 'john@example.com'
u.password 'funkypass'
u.password_confirmation 'funkypass'
u.first_name 'John'
u.last_name 'Smith'
u.billing_address "1 Cool St.\r\nBerkeley"
u.billing_state 'CA'
u.billing_zipcode '94519'
u.other_zipcode '28311'
u.phone '(925) 555-1212'
end
I created a Cucumber feature in features/user_sessions.feature:
Feature: User Sessions

So that I can blah, blah, blah
As a registered user
I want to log in and log out

Scenario: log in
Given I am a registered user
And I am on the homepage
When I login
Then I should see "Login successful!"
And I should see "Logout"

Scenario: log out
Given I am logged in
And I am on the homepage
When I follow "Logout"
Then I should see "Logout successful!"
And I should see "Register"
And I should see "Log In
To implement this, I created step definitions in features/step_definitions/user_sessions_steps.rb
def user
@user ||= Factory :user
end

def login
user
visit path_to("the homepage")
response.should contain("Log In")
click_link "Log in"
fill_in "Username", :with => "john"
fill_in "Password", :with => "funkypass"
click_button "Login"
response.should contain("Login successful!")
response.should contain("Logout")
end

Given /^I am a registered user$/ do
user
end

When /^I login$/ do
login
end

Given /^I am logged in$/ do
login
end
That's it.

Authlogic's testing documentation talks about stuff like:
require "authlogic/test_case"
...
include Authlogic::TestCase
...
setup :activate_authlogic
UserSession.create(users(:whomever)) # logs a user in
In theory, you should be able to log a user in using some code--without going through the login form. I worked on this for several hours, but I wasn't able to get it to work. I googled quite a bit, but I wasn't able to find anyone else who could get this to work (i.e. integrate RSpec, Cucumber, Authlogic, and factory_girl). It seems that everyone else came to the same conclusion I did--just force the tests to use the login form.

7 comments:

Brandon Craig Rhodes said...

Could you filter your posts so that Ruby-only material does not wind up on Planet Python? Thanks!

Shannon -jj Behrens said...

See the next post.

Sarah Allen said...

so... how is the factory better than a plain old fixture?

Shannon -jj Behrens said...

> so... how is the factory better than a plain old fixture?

1) The test data can be very near the test. 2) You can have instances inherit from each other. That way you only need to specify the things that are different and interesting. 3) You don't have a single fixtures file growing completely out of hand.

Rainer said...

>UserSession.create(users(:whomever)) # logs a user in

This didn't work for me. I used UserSession.create(:login => 'user', :password => 'password') like in the UserSessions Controller, for a normal login.
This set the current_user. Unfortunately I could use Factory Girl, because an association for the user which was created before login, did not exist after the user was logged in.

Shannon -jj Behrens said...

As my tests are growing more and more complicated, I'm getting into situations where I don't know if I've already called Factory for a given user or not. Originally, I wrote code that would try to fetch the user from the database, and if that failed, it would call Factory. However, I found out that the object returned by Factory has a password, whereas the object returned by User.find_by_username! doesn't. Hence, I wrote the following function:

# Call "Factory user" and cache the results in the @users hash.
#
# user should be a symbol.
#
# This function serves two purposes. First of all, it prevents the same
# user from being passed to Factory twice, which would cause an error. Second
# of all, it returns the original factory object rather than looking up the
# record in the database. That's important because you can't get the password
# out of the database, and if you can't get the password out of the database,
# you can't use it to log into the site in your tests.
def user_factory(user)
user = user.to_sym # Just to be sure.
@users ||= {}
@users[user] ||= Factory user
en

Shannon -jj Behrens said...

reset_session and Webrat don't play nicely with one another. See https://webrat.lighthouseapp.com/projects/10503/tickets/383-reset_session-and-webrat-dont-play-nicely-with-one-another for a workaround.