Thursday, November 22, 2007

Interesting. The book I'm going through for rails has added login functionality to adding a new story to the site. I've added a before_filter for the new action on StoryController.

before_filter :login_required, :only => :new

Now, though, my StoryController tests fail that look at getting the new form. The :login_required filter looks like

def logged_in?
    ! @current_user.blank?
end
helper_method :logged_in?
def login_required
    return true if logged_in?
    session[:return_to] = request.request_uri
    redirect_to :controller => "/account", :action => "login" and
        return false
end

Well, unfortunately, in order to get a user logged in, I need to call the :login action on the AccountController. The book didn't include this, so my functional tests for StoryController are failing, as a test like

def test_should_show_new_form
    get :new
    assert_select 'form p', :count => 3
end

doesn't have form with 3 p elements. I had a thought that it was getting redirected, so I changed the test to

def test_should_show_new_form
    get :new
    assert_redirected_to :controller=>"/account", :action=>'login'
end

and it passes. So, it appears that the before_filter is working.

So, I need to fake out the filter somehow; I need to set up a logged on user. The log on looks like:

before_filter :fetch_logged_in_user
protected
def fetch_logged_in_user
    return if session[:user_id].blank?
    @current_user = User.find_by_id(session[:user_id])
end

I am not 100% sure if I need to fake the session[:user_id] or just have a @current_user. I tried both, actually, and

session[:user_id] = 1

throws an exception in the test.

@current_user = users(:corey)

doesn't work either.

I figured that I could call the login action with credentials from users(:corey). I first tried

post :controller=>'/account',:action=>'login', :login => users(:corey).login, :password => users(:corey).password

at the top of the my test, hoping that it would call the action and get my system logged in. Here's the exception I got:

ActionController::UnknownAction: No action responded to passwordCorey Hainesactionlogincontroller/accountlogincorey

That is a really strange error. Looking at it, I think my understanding of the users fixture may not be getting what I think, so I'll try this:

post :controller=>'/account',:action=>'login', :login => 'patrick', :password => 'sekrit'

Nope, that didn't work either. So, my I do understand users. Looking closer at the exception, I'm guessing that I may just have the parameters for post wrong.

Off I go to http://api.rubyonrails.com/ to look up post.

post(path, parameters=nil,headers=nil)

Doesn't look like the post that I'm using. Doh! Of course it isn't, since I'm using the post from the testing framework. Looking up on google, I finally find a post called "#post method in tests with a different controller."

It turns out that you need to redefine the controller, then call post, then reset the controller. Let's try it. I changed the test to the following code:

def test_should_show_new_form
    old_controller = @controller
    @controller = AccountController.new
    post :login, :login => 'patrick', :password => 'sekrit'

    @controller = old_controller
    get :new
    assert_select 'form p', :count => 3
end

I ran the test, and, voila, it worked! So, now, I just need to add this to the other tests that are failing. I have a test to check adding a story:

def test_should_add_story
    post :new, :story => {
        :name=>'test story',
        :link=>'http://www.test.com/'
    }
    assert ! assigns(:story).new_record?
    assert_redirected_to :action=>'index'
    assert_not_nil flash[:notice]
end

Let me change it to support logging in:

def test_should_add_story
    old_controller = @controller
    @controller = AccountController.new
    post :login, :login => 'patrick', :password => 'sekrit'

    post :new, :story => {
        :name=>'test story',
        :link=>'http://www.test.com/'
    }
    assert ! assigns(:story).new_record?
    assert_redirected_to :action=>'index'
    assert_not_nil flash[:notice]
end

HUZZAH! It worked! Let me pull it out into a new method:

def login_user   
    old_controller = @controller
    @controller = AccountController.new
    post :login, :login => 'patrick', :password => 'sekrit'

    @controller = old_controller
end

This seems like a fantastic helper method execute_under_different_controller(controller, code). I'm not 100% sure how to build this method, so I'm going to take a minute to learn how to do it in Ruby.

We now have a nice method called execute_different_controller, so changed login_user to

def login_user   
    execute_under_different_controller AccountController do
                            post :login, :login => 'patrick', :password => 'sekrit'
            end
end 

I don't like the fact that I'm duplicating the data from my users.yaml file in my tests. If I load the fixture, I should be able to reference users(:patrick) here. Let's try it.

def login_user   
    user = users(:patrick)
    execute_under_different_controller AccountController do
                            post :login, :login => user.login, :password => user.password
            end
end

 Well, that worked. I guess I can understand that the book doesn't push too hard on this point, but it is a crazy, dangerous amount of duplication in your tests. This sort of data duplication leads to fragile tests.

After adding login_user to the tests for StoryController, things are back on track to continue through the book.

Thursday, November 22, 2007 12:04:21 PM (Eastern Standard Time, UTC-05:00)  #    Comments [0]