RSpec On Rails
Liquid error: undefined method `login' for nil:NilClass : October 29th, 2006
RSpec is mere days away from a new release with greatly improved Rails support.
Since people are currently paying me to write Rails code, rather than plain old standalone Ruby (hint hint), I've been waiting for these features before making serious use of RSpec.
As an exercise, I 'ported' the acts_as_authenticated controller tests to RSpec. The results were fairly interesting. Subjectively, I find it more readable than the test/unit version. Objectively, one of the test/unit test cases doesn't get run because there are two methods with the same name.
In RSpec, you make method calls to define specs, rather than defining methods with specific names. This makes them 'first class' Ruby constructs, immune to this kind of invisible error.
Here's the code from acts_as_authenticated:
def test_should_fail_cookie_login
users(:quentin).remember_me
users(:quentin).update_attribute :remember_token_expires_at, 15.minutes.ago.utc
@request.cookies["auth_token"] = cookie_for(:quentin)
get :index
assert !@controller.send(:logged_in?)
end
def test_should_fail_cookie_login
users(:quentin).remember_me
@request.cookies["auth_token"] = auth_token('invalid_auth_token')
get :index
assert !@controller.send(:logged_in?)
end
Only the second method is actually run, because it redefines the first method. test/unit has no way of knowing this, of course.
Here's the RSpec equivalent:
specify "should fail to login with expired cookie" do
users(:quentin).remember_me
users(:quentin).update_attribute :remember_token_expires_at, 15.minutes.ago
request.cookies["auth_token"] = cookie_for(:quentin)
get :index
controller.should_not_be_logged_in
end
specify "should fail to login with invalid cookie" do
users(:quentin).remember_me
request.cookies["auth_token"] = auth_token('invalid_auth_token')
get :index
controller.should_not_be_logged_in
end
So far, so good. Anyone who thinks that RSpec is 'only' about a different set of terminology should give it a serious try first.
Another fun trick is the output of 'rake spec:doc':
The Account controller - should be an AccountController - should redirect after successful login - should not redirect after failed login - should allow signup - should require login on signup - should require password on signup - should require password confirmation on signup - should require email on signup - should log out when requested - should remember me - should not remember me - should delete auth token on logout - should login with cookie - should fail to login with expired cookie - should fail to login with invalid cookieJust in case anyone is actually interested, here's the full spec:
require File.dirname(__FILE__) + '/../spec_helper'
def login_as(user)
request.session[:user] = user ? users(user).id : nil
end
def create_user(options = {})
post :signup, :user => { :login => 'quire', :email => 'quire@example.com',
:password => 'quire', :password_confirmation => 'quire' }.merge(options)
end
def auth_token(token)
CGI::Cookie.new('name' => 'auth_token', 'value' => token)
end
def cookie_for(user)
auth_token users(user).remember_token
end
context "The Account controller" do
fixtures :users
controller_name :account
specify "should be an AccountController" do
controller.should_be_an_instance_of AccountController
end
specify "should redirect after successful login" do
post :login, :login => 'quentin', :password => 'test'
session[:user].should_not_be_nil
response.should_be_redirect
end
specify "should not redirect after failed login" do
post :login, :login => 'quentin', :password => 'bad password'
session[:user].should_be_nil
response.should_be_success
end
specify "should allow signup" do
expected_users = User.count + 1
create_user
response.should_be_redirect
User.count.should == expected_users
end
specify "should require login on signup" do
expected_users = User.count
create_user(:login => nil)
assigns(:user).errors.on(:login).should_not_be_nil
response.should_be_success
end
specify "should require password on signup" do
expected_users = User.count
create_user(:password => nil)
assigns(:user).errors.on(:password).should_not_be_nil
response.should_be_success
end
specify "should require password confirmation on signup" do
expected_users = User.count
create_user(:password_confirmation => nil)
assigns(:user).errors.on(:password_confirmation).should_not_be_nil
response.should_be_success
end
specify "should require email on signup" do
expected_users = User.count
create_user(:email => nil)
assigns(:user).errors.on(:email).should_not_be_nil
response.should_be_success
end
specify "should log out when requested" do
login_as :quentin
get :logout
session[:user].should_not_be_nil
response.should_be_redirect
end
specify "should remember me" do
post :login, :login => 'quentin', :password => 'test', :remember_me => '1'
response.cookies["auth_token"].should_not_be_nil
end
specify "should not remember me" do
post :login, :login => 'quentin', :password => 'test', :remember_me => '0'
response.cookies["auth_token"].should_be_nil
end
specify "should delete auth token on logout" do
login_as :quentin
get :logout
response.cookies["auth_token"].should == []
end
specify "should login with cookie" do
users(:quentin).remember_me
request.cookies["auth_token"] = cookie_for(:quentin)
get :index
controller.should_be_logged_in
end
specify "should fail to login with expired cookie" do
users(:quentin).remember_me
users(:quentin).update_attribute :remember_token_expires_at, 15.minutes.ago
request.cookies["auth_token"] = cookie_for(:quentin)
get :index
controller.should_not_be_logged_in
end
specify "should fail to login with invalid cookie" do
users(:quentin).remember_me
request.cookies["auth_token"] = auth_token('invalid_auth_token')
get :index
controller.should_not_be_logged_in
end
end
14 Responses to “RSpec On Rails”
Sorry, comments are closed for this article.
October 29th, 2006 at 07:29 AM
Thanks for the great write-up. I recently made the switch to rspec and I’ve had a great time porting tests to specs and starting to think in terms of BDD.
What I’m most excited about is the the ease with which non-coders can start writing specs that I can turn into code (that and the easy integration with RCov).
I’ve just checked out the project to see if I can help add some support for edge rails and restful routes. Given how easy and intuitive it is I’d love to see this make it’s way towards core.
October 29th, 2006 at 07:33 AM
Nice to see RSpec getting more press, though I do have some problems with that controller spec. One of the points of BDD, which is what RSpec is for, is that you break down your contexts by fixture/state. Not only are controllers way too large in scope to be considered a “unit” in traditional testing terms, but in the case of BDD a single action can often be broken down into several contexts – the context being the request that is sent and the parameters sent with that request.
One of the nice things about the new RESTful approach being advocated is that it makes breaking your controller specs up into contexts a piece of cake. Consider a sessions controller, used for restful authentication:
And for a failed authentication:
This allows for much finer-grained contexts and is more in the spirit of BDD IMO.
October 29th, 2006 at 07:36 AM
Something else I should have mentioned is the emphasis of behaviour (hence behaviour-driven development). We aren’t just interested in testing that we can successfully login with our sessions controller (and therefore have the kind of meaningless test names that are typical with Rails e.g. “test_can_login”), but what actual behaviour is expected when we successfully login, i.e. we flash a success message, we redirect to the home page, we store the user’s id etc…
October 29th, 2006 at 07:38 AM
Luke: I agree with you. My code isn’t meant to be an example of RESTful authentication, etc.. just a way to integrate acts_as_authenticated into an RSpec project.
Your specs look good, though. Maybe you should release a sessions plugin/generator? :)
October 29th, 2006 at 08:52 AM
Well, it is a different set of terminology. Though, I can see the value of something like @name.should_equal ‘bob’ vs assert_equal ‘bob’, @name.
October 29th, 2006 at 09:45 AM
Defiler: sorry, I know that your code wasn’t meant to be RESTful, I don’t think I was clear in that respect. I simply took a RESTful sessions controller as an example as it is something I did recently and was the first thing that came to mind. I have been meaning to follow up my original BDD article and have also been meaning to cover controller specs so I might integrate a simple sessions controller into part two. As for a plugin, I’ve not used it but doesn’t Rick’s restful authentication plugin deal with this?
I do have a simple, crypted authentication plugin that is covered by BDD-style specs (but using test::unit for portability reasons), although that only takes care of the model side of things. You can find that here.
What I was trying to get at is that its important, when approaching things using BDD, to isolate and assert different behaviour depending on context, and in the case of controllers, that could be many different contexts, I’d say at least 2 per action, sometimes more.
October 29th, 2006 at 04:32 PM
I am still in the dark ages and using test/unit + an agiledox rake script to generate documents.
I have found it useful to use my tests to define all features, including those which are not implemented but will be in the future. Rather than keep a TODO list, I just create a new test. But the test is named differently…
The tests ignore it, but the the agiledox script documents it as a future feature. Perhaps others would find this concept useful, or maybe there is something similar in existance?
With RSpec instead of using `specify` you could have `eventually` or `will` or some other command to define unimplemented features to be documented but not tested.
October 30th, 2006 at 12:22 AM
@Defiler, nice article on RSpec, I have been using RSpec for awhile now for my Rails development and I could not go back to test unit. I do agree with Luke, “The Account controller” is do big of a context, I usually end up with about 4 contexts per action.
@J.W., I disagree with the approach of defining not implemented features in tests. Your tests should be testing the current behavior of the system. I think if you want to capture all the specs of your application in your tests, then your tests should be failing until those features are implemented, otherwise you are just hiding behavior in your tests.
November 1st, 2006 at 01:10 AM
Luke: I’ve been eagerly awaiting a BDD followup on your site ever since you’ve posted the first article. I think I may just jump in and start using BDD with my next project, but more info as to how you use it with rails would be appreciated.
In that vein, this post was also extremely useful. Examples are always very informative.
November 2nd, 2006 at 06:19 AM
“RSpec is mere days away from a new release with greatly improved Rails support.”
How are you keeping up with the development of rSpec? Are you just watching the changelogs? I am very interested in BDD and the development of rSpec, but I can’t seem to find very much information on it.
November 8th, 2006 at 12:53 PM
Meekish: are you subscribed to the mailing lists (devel and users)?
May 17th, 2007 at 01:54 AM
Nice
May 17th, 2007 at 04:07 AM
Nice…
May 17th, 2007 at 08:39 AM
interesting