f l a m e . o r g

organized flames

How I test Ruby on Rails with RSpec and Cucumber

Posted on November 19, 2009

In any non-trivial application, I end up with several things in common.

  • Generic pages which anyone can see. (I usually make this the About controller)
  • Users
  • Things (DNS Zones, pictures, etc)
  • Access restrictions to those users and things
  • Tests to ensure access control to those users and things

When to use RSpec, and when to use Cucumber

I use RSpec for unit tests, and some low-level controller tests. I specifically do not use RSpec for “user experience” or multiple-step testing, so no views are tested using RSpec.

Cucumber is used for all things “user experience.” It will test that a user cannot access other’s pages, etc. but it doesn’t do anything that can’t easily be done by filling in forms or changing the URL.

Types of users

In my world, there are three kinds of users which appear over and over again.

  • Guests. These guys can look at anything in the About controller.
  • Logged-in users. These can look at anything they own and some things that they do not. They can edit anything they own (with security restrictions on some fields.)
  • Administrators. These are the ones who can look at and modify anything. I usually have an admin rights on/off toggle, and try hard to make what they see close to what users would see.

User tests

  • Tests to ensure that the public cannot do things.
  • Tests to ensure that a logged-in user cannot poke around in someone else’s business using usual page contents.
  • Tests to ensure the the public or a logged in user cannot use specially crafted requests to break things.
  • Tests to ensure that a user can change their own stuff.
  • Tests to ensure that an admin can do pretty much everything.

Most of these tests are a combination of RSpec and Cucumber. The “hack” type tests (like directly fiddling around with assignments which my normal forms may limit to only items the logged in user has access to) are best done with RSpec.

Examples

No blog post can possibly be useful without examples. They use Factory Girl to generate a user, which has an email, password, and an admin flag.

spec/spec_helper.rb snippit

 1 def login_user(options = {})
 2   @logged_in_user = Factory.create(:user, options)
 3   @controller.stub!(:current_user).and_return(@logged_in_user)
 4   @logged_in_user
 5 end
 6 
 7 def login_admin(options = {})
 8   options[:admin] = true
 9   @logged_in_user = Factory.create(:user, options)
10   @controller.stub!(:current_user).and_return(@logged_in_user)
11   @logged_in_user
12 end
13 
14 def logout_user
15   @logged_in_user = nil
16   @controller.stub!(:current_user).and_return(@logged_in_user)
17   @logged_in_user
18 end

My complete spec/controllers/users_controller_spec.rb file

Sorry for the length of this part.

  1 require 'spec_helper'
  2 
  3 describe UsersController do
  4   setup :activate_authlogic
  5 
  6   before(:each) do
  7     logout_user
  8     @other_user = Factory.create(:user)
  9   end
 10 
 11   def mock_user(stubs={})
 12     @mock_user ||= mock_model(User, stubs)
 13   end
 14 
 15   def make_users
 16     users = [ @other_user ]
 17     users << @logged_in_user if @logged_in_user
 18     4.times do
 19       users << Factory.create(:user)
 20     end
 21     users  
 22   end
 23 
 24   describe "GET index" do
 25     it "assigns all users as @users (admin)" do
 26       login_admin
 27       users = make_users
 28       get :index
 29       assigns[:users].sort.should == users.sort
 30     end
 31 
 32     it "tells me to bugger off (not admin)" do
 33       login_user
 34       users = make_users
 35       get :index
 36       flash[:error].should match "You must be an administrator to access this page."
 37       response.should redirect_to(root_path)
 38     end
 39 
 40     it "tells me to bugger off (not logged in)" do
 41       users = make_users
 42       get :index
 43       flash[:error].should match "You must be an administrator to access this page."
 44       response.should redirect_to(root_path)
 45     end
 46   end
 47 
 48   describe "GET show" do
 49     it "assigns myself as @user (admin)" do
 50       login_admin
 51       get :show, :id => @logged_in_user.id
 52       assigns[:user].should == @logged_in_user
 53     end
 54 
 55     it "assigns me as @user (my data)" do
 56       login_user
 57       get :show, :id => @logged_in_user.id
 58       assigns[:user].should == @logged_in_user
 59     end
 60 
 61     it "assigns the requested user as @user (admin)" do
 62       login_admin
 63       get :show, :id => @other_user.id
 64       assigns[:user].should == @other_user
 65     end
 66 
 67     it "tells me to bugger off (not admin)" do
 68       login_user
 69       get :show, :id => @other_user.id
 70       flash[:error].should match "You must be an administrator to access this page."
 71     end
 72 
 73     it "redirects to root (not admin)" do
 74       login_user
 75       get :show, :id => @other_user.id
 76       response.should redirect_to(root_path)
 77     end
 78 
 79     it "tells me to log in (not logged in)" do
 80       get :show, :id => @other_user.id
 81       flash[:error].should match "Please log in to access this page."
 82     end
 83 
 84     it "redirects to root (not logged in)" do
 85       get :show, :id => @other_user.id
 86       response.should redirect_to(login_path)
 87     end
 88   end
 89 
 90   describe "GET new" do
 91     it "assigns a new user as @user (not logged in)" do
 92       User.stub!(:new).and_return(mock_user)
 93       get :new
 94       assigns[:user].should equal(mock_user)
 95     end
 96   end
 97 
 98   describe "GET edit" do
 99     it "assigns me as @user (admin)" do
100       login_admin
101       get :edit, :id => @logged_in_user.id
102       assigns[:user].should == @logged_in_user
103     end
104 
105     it "assigns the requested user as @user (admin)" do
106       login_admin
107       get :edit, :id => @logged_in_user.id
108       assigns[:user].should == @logged_in_user
109     end
110 
111     it "assigns me as @user (my data)" do
112       login_user
113       get :edit, :id => @logged_in_user.id
114       assigns[:user].should == @logged_in_user
115     end
116 
117     it "tells me to bugger off (not admin)" do
118       login_user
119       get :edit, :id => @other_user.id
120       flash[:error].should match "You must be an administrator to access this page."
121       end
122 
123     it "redirects to root (not admin)" do
124       login_user
125       get :edit, :id => @other_user.id
126       response.should redirect_to(root_path)
127     end
128 
129     it "tells me to log in (not logged in)" do
130       get :edit, :id => @other_user.id
131       flash[:error].should match "Please log in to access this page."
132     end
133 
134     it "redirects to login (not logged in)" do
135       get :edit, :id => @other_user.id
136       response.should redirect_to(login_path)
137     end
138   end
139 
140   describe "PUT update" do
141     describe "with valid params" do
142       it "assigns me as @user (my data)" do
143         login_user
144         put :update, :id => @logged_in_user.id
145         assigns[:user].should == @logged_in_user
146       end
147 
148       it "tells me to bugger off (not admin)" do
149         login_user
150         put :update, :id => @other_user.id
151         flash[:error].should match "You must be an administrator to access this page."
152       end
153 
154       it "tells me to log in (not logged in)" do
155         get :edit, :id => @other_user.id
156         flash[:error].should match "Please log in to access this page."
157       end
158 
159       it "assigns the requested user as @user (admin)" do
160         login_admin
161         put :update, :id => @logged_in_user.id
162         assigns[:user].should == @logged_in_user
163       end
164 
165       it "redirects to the user (admin)" do
166         login_admin
167         put :update, :id => @logged_in_user.id
168         response.should redirect_to(user_url(@logged_in_user))
169       end
170 
171       it "redirects to me (my data)" do
172         login_user
173         put :update, :id => @logged_in_user.id
174         response.should redirect_to(user_url(@logged_in_user))
175       end
176 
177       it "redirects to root (not admin)" do
178         login_user
179         put :update, :id => @other_user.id
180         response.should redirect_to(root_path)
181       end
182 
183       it "redirects to login (not logged in)" do
184         put :update, :id => @other_user.id
185         response.should redirect_to(login_path)
186       end
187 
188       it "can edit anyone's data (admin)" do
189         login_admin
190         put :update, :id => @other_user.id, :user => { :email => "new_email@example.com"}
191         response.should redirect_to(user_url(@other_user))
192         assigns[:user].email.should == "new_email@example.com"
193       end
194 
195       it "can edit my own data (not admin)" do
196         login_user
197         put :update, :id => @logged_in_user.id, :user => { :email => "new_email@example.com"}
198         response.should redirect_to(user_url(@logged_in_user))
199         assigns[:user].email.should == "new_email@example.com"
200       end
201     end
202 
203     describe "with invalid params" do
204       it "updates the requested user (admin)" do
205         login_admin
206         User.should_receive(:find).with("37").and_return(mock_user)
207         mock_user.should_receive(:update_attributes).with({'these' => 'params'})
208         put :update, :id => "37", :user => {:these => 'params'}
209       end
210 
211       it "assigns the user as @user (admin)" do
212         login_admin
213         User.stub!(:find).and_return(mock_user(:update_attributes => false))
214         put :update, :id => "1"
215         assigns[:user].should equal(mock_user)
216       end
217 
218       it "re-renders the 'edit' template (admin)" do
219         login_admin
220         User.stub!(:find).and_return(mock_user(:update_attributes => false))
221         put :update, :id => "1"
222         response.should render_template('edit')
223       end
224     end
225 
226   end
227 
228   describe "DELETE destroy" do
229     it "destroys the requested user (admin)" do
230       login_admin
231       User.should_receive(:find).with("37").and_return(mock_user)
232       mock_user.should_receive(:destroy)
233       delete :destroy, :id => "37"
234     end
235 
236     it "redirects to the users list (admin)" do
237       login_admin
238       User.stub!(:find).and_return(mock_user(:destroy => true))
239       delete :destroy, :id => "1"
240       response.should redirect_to(users_url)
241     end
242 
243     it "destroys me (my data)" do
244       login_user
245       delete :destroy, :id => @logged_in_user.id
246     end
247 
248     it "tells me to bugger off (not admin)" do
249       login_user
250       delete :destroy, :id => @other_user.id
251       flash[:error].should match "You must be an administrator to access this page."
252     end
253 
254     it "tells me to log in (not logged in)" do
255       delete :destroy, :id => @other_user.id
256       flash[:error].should match "Please log in to access this page."
257     end
258 
259     it "redirects to root (not admin)" do
260       login_user
261       delete :destroy, :id => @other_user.id
262       response.should redirect_to(root_path)
263     end
264 
265     it "redirects to login (not logged in)" do
266       delete :destroy, :id => @other_user.id
267       response.should redirect_to(login_path)
268     end
269   end
270 end