[rspec-users] Specing based on user roles

Jesse Clark jesse at jesseclark.com
Wed Nov 12 19:12:21 EST 2008


On Nov 6, 2008, at 8:01 AM, David Chelimsky wrote:

> I've really moved away from shared example groups and started writing
> more targeted macros. So I might do something like this:
>
> def for_roles *roles
>  roles.each do |role|
>    before(:each) { login_as role }
>    yield
>  end
> end
>
> describe OrdersController do
>  describe "GET index" do
>    for_roles :admin, :sysadmin do |role|
>      it "..." do ... end
>    end
>    for_roles :sysadmin do |role|
>      it "..." do ... end
>    end
>  end

I was attempting to follow this example and discovered that  
before(scope, &block) is an alias for append_before which, as the  
method name indicates, appends the before block to the existing  
collection of before blocks.

So, in your example code above it seems that login_as would get called  
for each role passed to for_roles before each example is executed.  
Since the before blocks are stored internally as an array, the last  
element of the array would win.

Another example:

require File.expand_path(File.dirname(__FILE__) + '/../../spec_helper')

def for_roles *roles
   roles.each do |role|
     before(:each) do
       puts role.to_s
     end
     yield
   end
end

describe "DRY roles test" do

   describe "GET index" do
     for_roles :admin, :sysadmin do |role|
       it "..." do
         puts 'in example'
       end
     end
   end

end

Running this sample spec from the command line generates this output:

admin
sysadmin
in example
.admin
sysadmin
in example
.

I noticed that there is a method called 'remove_after' in  
before_and_after_hooks.rb but no corresponding remove_before. Looking  
at the code:

       def remove_after(scope, &block)
         after_each_parts.delete(block)
       end

I noticed that this method doesn't use the scope parameter at all and  
just attempts to delete the block from after_each_parts. So, I  
modified the method to honor the scope  parameter and wrote a  
corresponding remove_before method:

       def remove_after(scope, &block)
         parts = after_parts_from_scope(scope)
         parts.delete(block)
       end

       def remove_before(scope, &block)
         parts = before_parts_from_scope(scope)
         parts.delete(block)
       end

However, this doesn't work as expected because if you do something like:

before(:each) { login_as role }
remove_before(:each) { login_as role }

two different Proc objects are created by each method call so  
parts.delete(block) will always fail. The only way I could see around  
this was if there were versions of append_before and remove_before  
which took a Proc object as a parameter instead of converting the  
block so you could maintain a reference to it in the calling code, i.e.

       def append_before_proc(*args, block)
         scope, options = scope_and_options(*args)
         parts = before_parts_from_scope(scope)
         parts << block
       end

       def remove_before_proc(scope, block)
         parts = before_parts_from_scope(scope)
         parts.delete(block)
       end

my_block = Proc.new { login_as role }
append_before_proc(:each, my_block)
remove_before_proc(:each, my_block)

Which seems like a lot of monkey patching to get this working. So now  
(finally) my questions:

1) Is there a better way to do this?
2) The current version of remove_after seems broken. Should I report  
this as a bug?

Thanks,
-Jesse


More information about the rspec-users mailing list