Search code examples
ruby-on-railsrubyunit-testingshoulda

How to test a named_scope that references a class attribute with Shoulda?


I have the following ActiveRecord classes:

class User < ActiveRecord::Base
  cattr_accessor :current_user
  has_many :batch_records
end

class BatchRecord < ActiveRecord::Base
  belongs_to :user

  named_scope :current_user, lambda {
    { :conditions => { :user_id => User.current_user && User.current_user.id } }
  }
end

and I'm trying to test the named_scope :current_user using Shoulda but the following does not work.

class BatchRecordTest < ActiveSupport::TestCase
  setup do
    User.current_user = Factory(:user)
  end

  should_have_named_scope :current_user,
                          :conditions => { :assigned_to_id => User.current_user }
end

The reason it doesn't work is because the call to User.current_user in the should_have_named_scope method is being evaluated when the class is being defined and I'm change the value of current_user afterwards in the setup block when running the test.

Here is what I did come up with to test this named_scope:

class BatchRecordTest < ActiveSupport::TestCase
  context "with User.current_user set" do
    setup do
      mock_user = flexmock('user', :id => 1)
      flexmock(User).should_receive(:current_user).and_return(mock_user)
    end

    should_have_named_scope :current_user,
                            :conditions => { :assigned_to_id => 1 }
  end
end

So how would you test this using Shoulda?


Solution

  • I think you are going about this the wrong way. Firstly, why do you need to use a named scope? Wont this just do?

    class BatchRecord < ActiveRecord::Base
      belongs_to :user
    
      def current_user
        self.user.class.current_user
      end
    end
    

    In which case it would be trivial to test. BUT! WTF are you defining current_user as a class attribute? Now that Rails 2.2 is "threadsafe" what would happen if you were running your app in two seperate threads? One user would login, setting the current_user for ALL User instances. Now another user with admin privileges logs in and current_user is switched to their instance. When the first user goes to the next page he/she will have access to the other persons account with their admin privileges! Shock! Horror!

    What I reccomend doing in this case is to either making a new controller method current_user which returns the current user's User instance. You can also go one step further and create a wrapper model like:

    class CurrentUser
    
      attr_reader :user, :session
    
      def initialize(user, session)
        @user, @session = user, session
      end
    
      def authenticated?
        ...
      end
    
      def method_missing(*args)
        user.send(*args) if authenticated?
      end
    
    end
    

    Oh, and by the way, now I look at your question again perhaps one of the reasons it isn't working is that the line User.current_user && User.current_user.id will return a boolean, rather than the Integer you want it to. EDIT I'm an idiot.

    Named scope is really the absolutely wrong way of doing this. Named scope is meant to return collections, rather than individual records (which is another reason this fails). It is also making an unnecessary call the the DB resulting in a query that you don't need.