Testing RESTful actions of multiple Rails controllers with RSpec can generate a lot of code repetition. The following code is my first attempt at using shared examples to DRY things up.
Here is what I don't like about the code, could not find a better way and would like your help to improve:
The shared examples require that specific variables are set within let blocks within the controller spec (high coupling). I have tried to use the model name to infer the factory name and create the test data within the share examples. It works well to create the record and records variables. However, some models require the presence of associations and FactoryGirl.attributes_for does not create associated records, so validation fails. So, valid_attributes are created differently for different models. The only (likely bad) way I could think of creating valid_attributes within shared examples is to pass a string containing the code used to create the attributes and evaluate it (eval) within the shared examples
The tests that assert redirection use eval to call Rails' route/path helpers. Different controllers in this app have different redirect behaviors. After creating or updating a record, some controllers redirect to the #show action, others to #index. The problem is that when expecting a redirect to #show, AFAIK, we have to know the record ID in order to build the expected URL. And we don't know the record ID within the controller spec. We only know it within the shared examples. So how can we pass an expected redirect URL from the controller spec to the shared example if we do not yet know what that URL is (because we don't know the record ID)?
Also, please let me know if you spot any additional issues.
The controller spec:
# spec/controllers/quotes_controller_spec.rb
require "rails_helper"
RSpec.describe QuotesController, :focus, :type => :controller do
login_admin
let(:model) { Quote }
let(:record) { FactoryGirl.create(:quote) }
let(:records) { FactoryGirl.create_pair(:quote) }
let(:valid_attributes) { FactoryGirl.attributes_for(:quote, quote: "New quote") }
let(:invalid_attributes) { valid_attributes.update(quote: nil) }
include_examples "GET #index"
include_examples "GET #show"
include_examples "GET #new"
include_examples "GET #edit"
include_examples "POST #create", "quote_path(assigns(:quote))"
include_examples "PATCH #update", "quote_url"
include_examples "DELETE #destroy", "quotes_url"
end
The shared examples:
# spec/support/shared_examples/controller_restful_actions.rb
def ivar_name(model, plural: false)
if plural
model.name.pluralize.underscore.to_sym
else
model.name.underscore.to_sym
end
end
def record_name(model)
model.name.underscore.to_sym
end
RSpec.shared_examples "GET #index" do
describe "GET #index" do
it "requires login" do
sign_out current_user
get :index
expect(response).to require_login
end
it "enforces authorization" do
get :index
expect(controller).to enforce_authorization
end
it "populates instance variable with an array of records" do
get :index
expect(assigns(ivar_name(model, plural: true))).to match_array(records)
end
end
end
RSpec.shared_examples "GET #show" do
describe "GET #show" do
it "requires login" do
sign_out current_user
get :show, id: record
expect(response).to require_login
end
it "enforces authorization" do
get :show, id: record
expect(controller).to enforce_authorization
end
it "assigns the requested record to an instance variable" do
get :show, id: record
expect(assigns(ivar_name(model))).to eq(record)
end
end
end
RSpec.shared_examples "GET #new" do
describe "GET #new" do
it "requires login" do
sign_out current_user
get :new
expect(response).to require_login
end
it "enforces authorization" do
get :new
expect(controller).to enforce_authorization
end
it "assigns a new record to an instance variable" do
get :new
expect(assigns(ivar_name(model))).to be_a_new(model)
end
end
end
RSpec.shared_examples "GET #edit" do
describe "GET #edit" do
let(:record) { FactoryGirl.create(factory_name(model)) }
it "requires login" do
sign_out current_user
get :edit, id: record
expect(response).to require_login
end
it "enforces authorization" do
get :edit, id: record
expect(controller).to enforce_authorization
end
it "assigns the requested record to an instance variable" do
get :edit, id: record
expect(assigns(ivar_name(model))).to eq(record)
end
end
end
RSpec.shared_examples "POST #create" do |redirect_path_helper|
describe "POST #create" do
it "requires login" do
sign_out current_user
post :create, { record_name(model) => valid_attributes }
expect(response).to require_login
end
it "enforces authorization" do
post :create, { record_name(model) => valid_attributes }
expect(controller).to enforce_authorization
end
context "with valid attributes" do
it "saves the new record in the database" do
expect{
post :create, { record_name(model) => valid_attributes }
}.to change(model, :count).by(1)
end
it "assigns a newly created but unsaved record to an instance variable" do
post :create, { record_name(model) => valid_attributes }
expect(assigns(ivar_name(model))).to be_a(model)
expect(assigns(ivar_name(model))).to be_persisted
end
it "redirects to #{redirect_path_helper}" do
post :create, { record_name(model) => valid_attributes }
expect(response).to redirect_to(eval(redirect_path_helper))
end
end
context "with invalid attributes" do
it "does not save the new record in the database" do
expect{
post :create, { record_name(model) => invalid_attributes }
}.not_to change(model, :count)
end
it "assigns a newly created but unsaved record an instance variable" do
post :create, { record_name(model) => invalid_attributes }
expect(assigns(ivar_name(model))).to be_a_new(model)
end
it "re-renders the :new template" do
post :create, { record_name(model) => invalid_attributes }
expect(response).to render_template(:new)
end
end
end
end
RSpec.shared_examples "PATCH #update" do |redirect_path_helper|
describe "PATCH #update" do
let(:record) { FactoryGirl.create(factory_name(model)) }
it "requires login" do
sign_out current_user
patch :update, { :id => record, record_name(model) => valid_attributes }
expect(response).to require_login
end
it "enforces authorization" do
patch :update, { :id => record, record_name(model) => valid_attributes }
expect(controller).to enforce_authorization
end
context "with valid attributes" do
it "updates the requested record" do
patch :update, { :id => record, record_name(model) => valid_attributes }
record.reload
expect(record).to have_attributes(valid_attributes)
end
it "assigns the requested record to an instance variable" do
put :update, { :id => record, record_name(model) => valid_attributes }
expect(assigns(ivar_name(model))).to eq(record)
end
it "redirects to #{redirect_path_helper}" do
patch :update, { :id => record, record_name(model) => valid_attributes }
expect(response).to redirect_to(eval(redirect_path_helper))
end
end
context "with invalid attributes" do
it "does not update the requested record" do
expect {
patch :update, { :id => record, record_name(model) => invalid_attributes }
}.not_to change { record.reload.attributes }
end
it "assigns the record to an instance variable" do
patch :update, { :id => record, record_name(model) => invalid_attributes }
expect(assigns(ivar_name(model))).to eq(record)
end
it "re-renders the :edit template" do
patch :update, { :id => record, record_name(model) => invalid_attributes }
expect(response).to render_template(:edit)
end
end
end
end
RSpec.shared_examples "DELETE #destroy" do |redirect_path_helper|
describe "DELETE #destroy" do
it "requires login" do
sign_out current_user
delete :destroy, id: record
expect(response).to require_login
end
it "enforces authorization" do
delete :destroy, id: record
expect(controller).to enforce_authorization
end
it "deletes the record" do
# Records are lazily created. Here we must force its creation.
record
expect{
delete :destroy, id: record
}.to change(model, :count).by(-1)
end
it "redirects to #{redirect_path_helper}" do
delete :destroy, id: record
expect(response).to redirect_to(eval(redirect_path_helper))
end
end
end
Probably not an answer but too long for a comment:
First of all you can wrap all of those in a shared_examples_for block e.g.
shared_examples_for 'a CRUD Controller' do
context "GET #index" do
it "requires login" do
sign_out current_user
get :index
expect(response).to require_login
end
####
end
context "GET #show" do
it "requires login" do
sign_out current_user
get :show, id: record
expect(response).to require_login
end
####
end
end
Secondly You can have shared examples inside shared examples to the above can be
shared_examples_for 'a CRUD Controller' do
shared_examples_for 'authenticatable' do |view:,params:{}|
it "requires login" do
sign_out current_user
get view, **params
expect(response).to require_login
end
end
context "GET #index" do
it_behaves_like 'authenticatable', view: :index
####
end
context "GET #show" do
it_behaves_like 'authenticatable', view: :show, id: record
####
end
end
Third you can assign variables inside a it_behaves_like block eg.
RSpec.describe QuotesController, :focus, :type => :controller do
login_admin
it_behaves_like 'a CRUD Controller' do
let(:model) { Quote }
let(:record) { FactoryGirl.create(:quote) }
let(:records) { FactoryGirl.create_pair(:quote) }
let(:valid_attributes) { FactoryGirl.attributes_for(:quote, quote: "New quote") }
let(:invalid_attributes) { valid_attributes.update(quote: nil) }
end
end
Fourth this too can be simplified
shared_examples_for 'a CRUD Controller' do |model:|
singular,plural = 2.times.map { |n| model.name.pluralize(n).underscore.to_sym }
let(:record) { FactoryGirl.create(singular)
let(:records) {FactoryGirl.create_pair(singular) }
let(:valid_attributes) do
# build should create the nested associations correctly as long
# as your factories are right
FactoryGirl.build(singular).attributes.delete_if do |k,_|
# this is because ActiveRecord#attributes contains columns
# you don't want to be considered updateable
["id","created_at","updated_at"].include?(k)
end
end
let(:invalid_attributes) do
# create an :invalid trait in your factory so that
# you don't have to worry about the model
FactoryGirl.build(singular, :invalid).attributes.delete_if do |k,_|
["id","created_at","updated_at"].include?(k)
end
end
####
end
RSpec.describe QuotesController, :focus, :type => :controller do
login_admin
it_behaves_like 'a CRUD Controller', model: Quote
end
Finally you are going to find that using a memoized let! will help drastically since you are creating an extraordinary amount of records in those tests as it stands now. This will degrade performance drastically and if you get to a model that has certain globally unique attributes your tests will fail everywhere.
Hopefully this helps start pointing you in the right direction
Update to control testing actions
shared_examples_for 'a CRUD Controller' do |model:|
accessible_method = ->(meth) { public_methods.include?(meth) }
context "GET #index", if: controller.method_defined?(:index) do
it_behaves_like 'authenticatable', view: :index
####
end
context "GET #show", if: controller.method_defined?(:show) do
it_behaves_like 'authenticatable', view: :show, id: record
####
end
end
Here is the improved code (based on engineersmnky's suggestions). Any suggestions for further improvements are welcome.
Controller spec:
# spec/controllers/quotes_controller_spec.rb
require "rails_helper"
RSpec.describe QuotesController, :type => :controller do
it_behaves_like "a CRUD controller",
model: Quote,
create_redirect_path_helper: "quote_path(assigns(:quote))",
update_redirect_path_helper: "quote_url",
delete_redirect_path_helper: "quotes_url"
end
Shared examples:
# spec/support/shared_examples/controller_restful_actions.rb
RSpec.shared_examples "a CRUD controller" do |model:,
create_redirect_path_helper:,
update_redirect_path_helper:,
delete_redirect_path_helper:|
def self.controller_has_action?(action)
described_class.action_methods.include?(action.to_s)
end
resource_singular = model.name.underscore.to_sym
resource_plural = model.name.pluralize.underscore.to_sym
before(:each) { login_admin }
let(:record) { FactoryGirl.create(resource_singular) }
let(:records) { FactoryGirl.create_pair(resource_singular) }
# Models that validate the presence of associated records require some
# hacking in the factory to include associations in the attributes_for output.
let(:valid_attributes) { FactoryGirl.attributes_for(resource_singular) }
# All factories must have a trait called :invalid
let(:invalid_attributes) do
FactoryGirl.attributes_for(resource_singular, :invalid)
end
describe "GET #index", if: controller_has_action?(:index) do
it "requires login" do
logout
get :index
expect(response).to require_login_web
end
it "enforces authorization" do
get :index
expect(controller).to enforce_authorization
end
it "populates ##{resource_plural} with an array of #{resource_plural}" do
# Force records to be created before the request.
records
get :index
# Required when testing the User model, or else the user created
# by the Devise login helper skews the result of this test.
expected_records = assigns(resource_plural) - [#current_user]
expect(expected_records).to match_array(records)
end
end
describe "GET #show", if: controller_has_action?(:show) do
it "requires login" do
logout
get :show, id: record
expect(response).to require_login_web
end
it "enforces authorization" do
get :show, id: record
expect(controller).to enforce_authorization
end
it "assigns the requested #{resource_singular} to an instance variable" do
get :show, id: record
expect(assigns(resource_singular)).to eq(record)
end
end
describe "GET #new", if: controller_has_action?(:new) do
it "requires login" do
logout
get :new
expect(response).to require_login_web
end
it "enforces authorization" do
get :new
expect(controller).to enforce_authorization
end
it "assigns a new #{resource_singular} to ##{resource_singular}" do
get :new
expect(assigns(resource_singular)).to be_a_new(model)
end
end
describe "GET #edit", if: controller_has_action?(:edit) do
it "requires login" do
logout
get :edit, id: record
expect(response).to require_login_web
end
it "enforces authorization" do
get :edit, id: record
expect(controller).to enforce_authorization
end
it "assigns #{resource_singular} to ##{resource_singular}" do
get :edit, id: record
expect(assigns(resource_singular)).to eq(record)
end
end
describe "POST #create", if: controller_has_action?(:create) do
it "requires login" do
logout
post :create, { resource_singular => valid_attributes }
expect(response).to require_login_web
end
it "enforces authorization" do
post :create, { resource_singular => valid_attributes }
expect(controller).to enforce_authorization
end
context "with valid attributes" do
it "saves the new #{resource_singular} in the database" do
expect{
post :create, { resource_singular => valid_attributes }
}.to change(model, :count).by(1)
end
it "assigns the saved #{resource_singular} to ##{resource_singular}" do
post :create, { resource_singular => valid_attributes }
expect(assigns(resource_singular)).to be_an_instance_of(model)
expect(assigns(resource_singular)).to be_persisted
end
it "redirects to #{create_redirect_path_helper}" do
post :create, { resource_singular => valid_attributes }
expect(response).to redirect_to(eval(create_redirect_path_helper))
end
end
context "with invalid attributes" do
it "does not save the new #{resource_singular} in the database" do
expect{
post :create, { resource_singular => invalid_attributes }
}.not_to change(model, :count)
end
it "assigns the unsaved #{resource_singular} to ##{resource_singular}" do
post :create, { resource_singular => invalid_attributes }
expect(assigns(resource_singular)).to be_a_new(model)
end
it "re-renders the :new template" do
post :create, { resource_singular => invalid_attributes }
expect(response).to render_template(:new)
end
end
end
describe "PATCH #update", if: controller_has_action?(:update) do
it "requires login" do
logout
patch :update, { :id => record,
resource_singular => valid_attributes }
expect(response).to require_login_web
end
it "enforces authorization" do
patch :update, { :id => record,
resource_singular => valid_attributes }
expect(controller).to enforce_authorization
end
context "with valid attributes" do
it "updates the requested #{resource_singular}" do
patch :update, { :id => record,
resource_singular => valid_attributes }
record.reload
# Required when testing Devise's User model with reconfirmable on
record.try(:confirm)
expect(record).to have_attributes(valid_attributes)
end
it "assigns the #{resource_singular} to ##{resource_singular}" do
put :update, { :id => record,
resource_singular => valid_attributes }
expect(assigns(resource_singular)).to eq(record)
end
it "redirects to #{update_redirect_path_helper}" do
patch :update, { :id => record,
resource_singular => valid_attributes }
expect(response).to redirect_to(eval(update_redirect_path_helper))
end
end
context "with invalid attributes" do
it "does not update the #{resource_singular}" do
# Do not attempt to "refactor" the following to any of the following:
# not_to change { quote }
# not_to change { quote.attributes }
# not_to have_attributes(invalid_attributes)
# None of the above will work. See
# https://github.com/rspec/rspec-expectations/issues/996#issuecomment-310729685
expect {
patch :update, { :id => record,
resource_singular => invalid_attributes }
}.not_to change { record.reload.attributes }
end
it "assigns the #{resource_singular} to ##{resource_singular}" do
patch :update, { :id => record,
resource_singular => invalid_attributes }
expect(assigns(resource_singular)).to eq(record)
end
it "re-renders the :edit template" do
patch :update, { :id => record,
resource_singular => invalid_attributes }
expect(response).to render_template(:edit)
end
end
end
describe "DELETE #destroy", if: controller_has_action?(:destroy) do
it "requires login" do
logout
delete :destroy, id: record
expect(response).to require_login_web
end
it "enforces authorization" do
delete :destroy, id: record
expect(controller).to enforce_authorization
end
it "deletes the #{resource_singular}" do
# Force record to be created before the `expect` block.
# Otherwise, it is both created and deleted INSIDE the block, causing the
# count not to change.
record
expect{
delete :destroy, id: record
}.to change(model, :count).by(-1)
end
it "redirects to #{delete_redirect_path_helper}" do
delete :destroy, id: record
expect(response).to redirect_to(eval(delete_redirect_path_helper))
end
end
end
For the let blocks, does it not work if you pass in the model as a parameter to the shared example like you do with the redirect_path_helper?
include_examples "GET #index", Quote
and then in your shared_example you can use the record_name method to create record and records from FactoryGirl and generate valid_attributes and invalid_attributes (you could create an :invalid_quote factory as well for invalid attributes, not sure if that's considered a good practice/idea with FactoryGirl though) from there.
For the second problem, you don't need to use the named route helpers, url_for(controller: :quote) and url_for(#quote) should both work.
I have a before_action filter and want to test that the index action is only executed if the user is logged in. Simply put, i don't know how to do this. I'm using my own simple authentication and i know i could use CanCan or similar but for my own learning i'm doing it the hard way!
ApplicationController.rb
helper_method :logged_in
helper_method :current_user
def current_user
#current_user ||= User.find_by_id(session[:current_user]) if session[:current_user]
end
def logged_in
unless current_user
redirect_to root_path
end
end
ActivitiesController.rb
before_action :logged_in
def index
#activities = Activity.all.where(user_id: #current_user)
end
Activities_Controller_spec.rb
require 'rails_helper'
RSpec.describe ActivitiesController, :type => :controller do
describe "GET index" do
before(:each) do
#activity = FactoryGirl.create(:activity)
session[:current_user] = #activity.user_id
#current_user = User.find_by_id(session[:current_user]) if session[:current_user]
end
it "shows all activities for signed in user" do
get :index, {user_id: #activity.user_id}
expect(response).to redirect_to user_activities_path
end
end
end
activities.rb(Factory)
FactoryGirl.define do
factory :activity do
association :user
title { Faker::App.name }
activity_begin { Faker::Date.forward(10) }
activity_end { Faker::Date.forward(24) }
end
end
I'm getting the following error:
Failure/Error: expect(response).to redirect_to user_activities_path
Expected response to be a redirect to <http://test.host/users/1/activities> but was a redirect to <http://test.host/>.
Expected "http://test.host/users/1/activities" to be === "http://test.host/".
After long discussion I think tests should be smth like this (it is not tested :) )
require 'rails_helper'
RSpec.describe ActivitiesController, :type => :controller do
describe "GET index" do
before(:each) do
#activity = FactoryGirl.create(:activity)
end
context 'when user is logged' do
before(:each) do
session[:current_user] = #activity.user_id
end
it "shows all activities for signed in user" do
get :index, {user_id: #activity.user_id}
expect(response).to be_success
end
end
context 'when user is anonymous' do
it "redirects user to root path" do
get :index, {user_id: #activity.user_id}
expect(response).to redirect_to root_path
end
end
end
end
just run a scaffold and then added the correspondent tests. For some reason UPDATE and POST method are not working and respond with "nil". Cant find why. User Controller has the basic REST methods. ApplicationController includes a before_filter:
before_filter :authenticate_user
protected
def authenticate_user
unless User.find_by_id(session[:user_id])
redirect_to root_path, notice: "Please log in"
end
end
user_controller_spec:
describe "POST create" do
describe "with valid params" do
it "creates a new User" do
expect {
post :create, {:user => valid_attributes}, valid_session
}.to change(User, :count).by(1)
end
it "assigns a newly created user as #user" do
post :create, {:user => valid_attributes}
expect(assigns(:user)).to be_a(User)
expect(assigns(:user)).to be_persisted
end
it "redirects to the created user" do
post :create, {:user => valid_attributes}
expect(response).to redirect_to(User.last)
end
end
describe "with invalid params" do
it "assigns a newly created but unsaved user as #user" do
allow_any_instance_of(User).to receive(:save).and_return(false)
post :create, {:user => { "email" => "invalid value" }}
expect(assigns(:user)).to be_a_new(User)
end
it "re-renders the 'new' template" do
allow_any_instance_of(User).to receive(:save).and_return(false)
post :create, {:user => { "email" => "invalid value" }}
expect(response).to render_template("new")
end
end
end
describe "PUT update" do
describe "with valid params" do
it "updates the requested user" do
user = FactoryGirl.create(:user)
expect_any_instance_of(User).to receive(:update).with({ "email" => "user#email.com" })
put :update, {:id => team.to_param, :user => { "email" => "user#email.com" }}
end
it "assigns the requested user as #user" do
user = FactoryGirl.create(:user)
put :update, {:id => user.to_param, :user => valid_attributes}
expect(assigns(:user)).to eq(user)
end
it "redirects to the user" do
user = FactoryGirl.create(:user)
put :update, {:id => user.to_param, :user => valid_attributes}
expect(response).to redirect_to(user)
end
end
describe "with invalid params" do
it "assigns the user as #user" do
user = FactoryGirl.create(:user)
allow_any_instance_of(User).to receive(:save).and_return(false)
put :update, {:id => user.to_param, :user => { "email" => "invalid value" }}
expect(assigns(:user)).to eq(user)
end
it "re-renders the 'edit' template" do
user = FactoryGirl.create(:user)
allow_any_instance_of(User).to receive(:save).and_return(false)
put :update, {:id => user.to_param, :user => { "name" => "invalid value" }}
expect(response).to render_template("edit")
end
end
end
Try to add render_views. Because RSpec tries to speed up specs and it does not render views by default.
I have this context in my spec file.
context 'get :index' do
it 'should be loaded successfully if current user is not customer' do
sign_in #creative
get :index
response.should be_success
end
it 'should redirect to root page if current user is customer' do
sign_in #customer
get :index
response.should redirect_to root_path
end
end
context 'post :create' do
it 'should be loaded successfully if current user is not customer' do
sign_in #creative
post :create
response.should be_success
end
it 'should redirect to root page if current user is customer' do
sign_in #customer
post :create
response.should redirect_to root_path
end
end
I repeat the same code in two different context.I convert it like this method but it doesn't work.
def check_user_sign_in(request_type, action)
context '#{request_type} :#{action}' do
it 'should be loaded successfully if current user is not customer' do
sign_in #creative
request_type action
response.should be_success
end
it 'should redirect to root page if current user is customer' do
sign_in #customer
request_type action
response.should redirect_to root_path
end
end
end
end
Here the problem is I didn't use parameter as a method name.
Do you know how can I use it with dry way?
This:
context '#{request_type} :#{action}' do
will not work because string interpolation is not evaluated inside single quotes.
You must use double quotes:
a = 'alpha' #=> "alpha"
b = 'beta' #=> "beta"
'#{a} #{b}' #=> "\#{a} \#{b}"
"#{a} #{b}" #=> "alpha beta"
Here is my current users_controller_spec.rb file
require 'spec_helper'
describe UsersController do
render_views
.
.
.
describe "success" do
before(:each) do
#attr = { :name => "New User", :email => "user#example.com",
:password => "foobar", :password_confirmation => "foobar" }
end
it "should create a user" do
lambda do
post :create, :user => #attr
end.should change(User, :count).by(1)
end
it "should redirect to the user show page" do
post :create, :user => #attr
response.should redirect_to(user_path(assigns(:user)))
end
end
end
end
When I run this I get the following:
Failures:
1) UsersController POST 'create' success should redirect to the user show page
Failure/Error: response.should redirect_to(user_path(user))
ActionController::RoutingError:
No route matches {:action=>"show", :controller=>"users"}
# ./spec/controllers/users_controller_spec.rb:95:in `block (4 levels) in <top (required)>'
Which leads me to believe that :user isn't an actual object. How can I test this and how can I change :user into an object that user_path understands.
Thanks in advance for any help.
UPDATED:
def create
#title = "Sign up"
#user = User.new(params[:user])
if #user.save
redirect_to #user, :notice => "Signed Up!"
else
#title = "Sign up"
render "new"
end
end
When I run the following:
it "should redirect to the user show page" do
post :create, :user => #attr
user = assigns(:user)
user.should_not be_blank
puts "user errors are: #{user.errors.full_messages.inspect}" unless user.is_valid?
user.should be_valid
response.should redirect_to(user_path(user))
end
I get:
1) UsersController POST 'create' success should redirect to the user show page
Failure/Error: user.should_not be_blank
expected blank? to return false, got true
# ./spec/controllers/users_controller_spec.rb:94:in `block (4 levels) in <top (required)>'
Try using assigns with brackets instead of parenthesis.
assigns[:user]
(RSpec docs)
EDIT
One pattern I always use when testing controller actions is to ensure that a variable required by the associated view(s) is assigned. For example,
it "should assign an #user variable" do
post :create, :user => #attr
assigns[:user].should_not be_nil
assigns[:user].should be_kind_of(User)
end
You've got a typo
response.should redirect_to(user_path(:user))
should be
response.should redirect_to(user_path(user))
Edit:
Try checking that the user is valid with:
it "should redirect to the user show page" do
post :create, :user => #attr
user = assigns(:user)
user.should_not be_blank
puts "user errors are: #{user.errors.full_messages.inspect}" unless user.is_valid?
user.should be_valid
response.should redirect_to(user_path(user))
end
I know it works on the previous test case... but still worth checking extensively at least once here. You can remove all that guff when you're certain.
In your test:
it "should redirect to the user show page" do
#user = Factory(:user)
post :create, :user => #attr
user = assigns(:user)
response.should redirect_to(user_path(#user))
end
Your #attr hash is different from #user. You are creating user with attributes of #attr, and asserting that it should redirect to #user show (which is different from one created). Change to:
it "should redirect to the user show page" do
post :create, :user => #attr
user = assigns(:user)
response.should redirect_to(user_path(user))
end