Specing based on user roles

On my website each user can have the following roles:

  1. Not logged in

  2. Logged in

  • active
  • administrator
  • sysadministrator

How would you write DRY specs to test each action of a controller?

Currently I am doing somethings that looks like:

describe ‘a non admin is signed in’, :shared => true do
before(:each) do
@current_user = mock_model(User, :id => 1, :state => ‘active’)
controller.stub!(:current_user).and_return(@current_user)
@current_user.should_receive(:administrator?).and_return(false)
@current_user.should_receive(:sysadministrator?).and_return(false)
end
end

describe ‘an administrator is signed in’, :shared => true do
before(:each) do
@current_user = mock_model(User, :id => 1, :state =>
‘administrator’)
controller.stub!(:current_user).and_return(@current_user)
@current_user.should_receive(:administrator?).and_return(true)
end
end

describe Admin::OrdersController, ‘index’ do

describe “A non admin wants to have access” do
it_should_behave_like ‘a non admin is signed in’

it "should redirect" do
  get :index
  response.should redirect_to(products_url)
end

end

describe “An admin wants to have access” do
it_should_behave_like ‘an administrator is signed in’

it "should render index page" do
  controller.should_receive(:select_date_initializer).with({},nil)
  Order.should_not_receive(:admin_find_from_params)

  get :index
end

it "should accept search params on index page" do
  Order.should_receive(:admin_find_from_params).with('test.host',

{}, {})

  get :index, :commit => 'Search'
end

end

end # describe Admin::OrdersController, ‘index’

I didn’t test for the sysadministrator as it would force me to repeat
all the tests for administrator, basically a sysadministrator has
administrator rights and more. What is a better way of writing such
specs?

On Thu, Nov 6, 2008 at 7:02 AM, Fernando P. [email protected]
wrote:

end

it “should accept search params on index page” do
I didn’t test for the sysadministrator as it would force me to repeat
all the tests for administrator, basically a sysadministrator has
administrator rights and more. What is a better way of writing such
specs?

Posted via http://www.ruby-forum.com/.


rspec-users mailing list
[email protected]
http://rubyforge.org/mailman/listinfo/rspec-users

One thing I’ve been trying lately is to share shared spec (somewhat like
inheritance). I too have many tests that a essentially duplicates of
each
other. So by factoring out commonalities and differences, I’d have
something
like

module NonAdminSpec
describe ‘acting like a non-admin’, :shared => true
before :each # log in as a non admin
describe ‘something that only applies to a NonAdmin’ do #
end

module

On Thu, Nov 6, 2008 at 10:02 AM, Fernando P. [email protected]
wrote:

end

it “should accept search params on index page” do
I didn’t test for the sysadministrator as it would force me to repeat
all the tests for administrator, basically a sysadministrator has
administrator rights and more.

And why wouldn’t you want to test that?

What is a better way of writing such specs?

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

describe “GET edit” do
for_roles :admin, :sysadmin do |role|
it “…” do … end
end
for_roles :sysadmin do |role|
it “…” do … end
end
end
end

When you’re doing this sort of thing, it is crucial that you keep
things organized so that individual actions can change independently
as requirements change. This is the thing that most people fail to
realize when they try to DRY things up.

This scheme makes it easy when we decide to remove a privilege from
admin but keep it in sysadmin. Just move that example (it “…”) to
the other for_roles block.

WDYT?

And why wouldn’t you want to test that?

I want to test for it, it’s just that I don’t want to copy/paste spec
like an idiot.

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

This scheme makes it easy when we decide to remove a privilege from
admin but keep it in sysadmin. Just move that example (it “…”) to
the other for_roles block.

WDYT?

I like your idea very much, very clean, readable and maintainable.

And thank you Mark, I will also use your idea to factor out
commonalities in my specs.

Mark W. to rspec-users
show details 7:46 AM (0 minutes ago)
Reply

  • Show quoted text -
    On Thu, Nov 6, 2008 at 7:02 AM, Fernando P. [email protected]
    wrote:

end

it “should accept search params on index page” do
I didn’t test for the sysadministrator as it would force me to repeat
all the tests for administrator, basically a sysadministrator has
administrator rights and more. What is a better way of writing such
specs?

Posted via http://www.ruby-forum.com/.


rspec-users mailing list
[email protected]
http://rubyforge.org/mailman/listinfo/rspec-users

One thing I’ve been trying lately is to share shared spec (somewhat like
inheritance). I too have many tests that a essentially duplicates of
each
other. So by factoring out commonalities and differences, I’d have
something
like

// anybody_spec.rb
module AnybodySpec
describe ‘anybody’, :shared => true
end

// nonadmin_spec.rb
require ‘anybody_spec’

describe ‘a nonadmin’
include AnyBodySpec
it_should_behave_like ‘anybody’
before :each # login as a nonadmin

specs that apply to anyone, given the right setup

end

// admin_spec.rb
require ‘anybody_spec’
module AdminSpec
include AnyBodySpec
describe ‘an admin’, :shared => true
it_should_behave_like ‘anybody’
# specs that apply to any admin, given the right setup
end

describe ‘admin’

specs that only apply to admins

end

// sysadmin_spec.rb
require ‘admin_spec’
module SysAdminSpec
include AdminSpec
describe ‘a sysadmin’
it_should_behave_like ‘an admin’
# specs that apply to any sysadmin, given the right setup
end

describe ‘sysadmin’

specs that only apply to sysadmins

end

This is all from memory, so it’s not complete. But the idea is that you
can
have shared specs behave_like other shared specs. before() is run from
bottom to top, so you can set @instance variables to parameterize the
specs,
just making sure that an outer level spec doesn’t overwrite an inner
level
spec’s variables.

I’m writing specs for a lot of different queries. There are three that
group
by date, week or month - they’re different in some ways and the same in
a
lot of others. Those three are grouped by time period, which is the same
in
some ways as grouping by an entity, and different from others. There are
two
or three other types of queries, too, each sharing some stuff and being
completely different in other ways.

Using nested shared examples like this has massively reduced the
duplication
in my spec code. And it’s also guaranteed that all the specs get run on
all
the code they apply to. Finally, it has suggested similar refactorings
in
the code under test. It seems to be working well for me.

///ark

On Thu, Nov 6, 2008 at 4:52 PM, Pat M. [email protected] wrote:

before(:each) { login_as role }
  it "..." do ... end

at the model level. No duplication, no fuss.

One thing I’ve noticed is that when I’m writing controller specs and
there seems to be too much duplication, I can usually eliminate it by
pushing some logic down the stack. In fancier terms, it means I need to
encapsulate a domain concept that I’ve missed up to that point.

+1

David

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

describe “GET edit” do
for_roles :admin, :sysadmin do |role|
it “…” do … end
end
for_roles :sysadmin do |role|
it “…” do … end
end
end
end

I can’t write my specs so that they work as expected. What does login_as
look like? And where do you put this code? I am not sure mine (if
working) gets initialized correctly.

I think I have to write my specs from scratch.

Fernando P. [email protected] writes:

end
end
commonalities in my specs.
A lot of the time when I’m faced with this issue - there’s a number of
roles, each with a set of privileges - I find it easier to focus on the
privileges rather than the roles themselves. This means creating
domain-specific methods on the user object. #can_edit_orders?,
#can_publish?, etc. Now when I’m specing at the controller level, I
only have to deal with two cases - when the user has the privilege, and
when he doesn’t. Then we write examples for all of the different roles
at the model level. No duplication, no fuss.

One thing I’ve noticed is that when I’m writing controller specs and
there seems to be too much duplication, I can usually eliminate it by
pushing some logic down the stack. In fancier terms, it means I need to
encapsulate a domain concept that I’ve missed up to that point.

Pat

On 2008-11-11, at 17:24, Fernando P. wrote:

describe OrdersController do
for_roles :admin, :sysadmin do |role|
login_as
look like? And where do you put this code? I am not sure mine (if
working) gets initialized correctly.

I think I have to write my specs from scratch.

Hi Fernando. #login_as is a custom method that you need to write
yourself. It should simply login as the given user, or a user with the
given role. Its implementation will depend on which authentication and
authorisation system you’re using.

Cheers,
Nick

Nick H. wrote:

On 2008-11-11, at 17:24, Fernando P. wrote:

describe OrdersController do
for_roles :admin, :sysadmin do |role|
login_as
look like? And where do you put this code? I am not sure mine (if
working) gets initialized correctly.

I think I have to write my specs from scratch.

Hi Fernando. #login_as is a custom method that you need to write
yourself. It should simply login as the given user, or a user with the
given role. Its implementation will depend on which authentication and
authorisation system you’re using.

Cheers,
Nick

I am using restful_authentication. I tried to look at their specs, but
they are unreadable. So I am trying to throw together my own
authentication mocker/stuber but with no luck.

I am using restful_authentication. I tried to look at their specs, but
they are unreadable. So I am trying to throw together my own
authentication mocker/stuber but with no luck.

Just to clear things out, can you tell me which snippet is correct:

get :index
response.should be_redirect

response.should be_redirect
get :index

It appears to me that 1) is correct, as 2) seems to be messing up all my
specs.

Fernando P. [email protected] writes:

response.should be_redirect
get :index

It appears to me that 1) is correct, as 2) seems to be messing up all my
specs.

Yep, #1

Pat

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

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