RSpec test destroy method (Rails Tutorial 3.2 Ch.

2019-03-09 11:26发布

问题:

Note: I've read this question and the answer, but for some reason the code isn't working for me. (see below for the error I'm getting)

Exercise 10 from Chapter 9 of the Rails Tutorial asks you to: Modify the destroy action [for users] to prevent admin users from destroying themselves. (Write a test first.)

The tricky part here is testing it, because the application already hides the "delete" link for the current user, so you have to do the http request directly.

I got the code working, and tested it by removing the piece of code that hides the delete link for the current user. Sure enough, if I click on the delete link for the currently logged-in user, it redirects me and gives me the notice message.

From users_controller.rb

  def destroy
    @user = User.find(params[:id])
    if current_user?(@user)
      redirect_to users_path, notice: "You can't destroy yourself."
    else
      @user.destroy
      flash[:success] = "User destroyed."
      redirect_to users_path
    end
  end

The problem I'm having is in writing the tests for this that will send the delete request and call the destroy method. I tried the solution from Rspec test for destroy if no delete link, which I'm copying here:

From user_pages_spec.rb

 describe "destroy" do
    let(:admin) { FactoryGirl.create(:admin) }

    it "should not allow the admin to delete herself" do
      sign_in admin
      #expect { delete user_path(admin), method: :delete }.should change(User, :count)
      expect { delete :destroy, :id => admin.id }.should_not change(User, :count)
    end
  end

But when I run this, I get this error from RSpec

Failures:

  1) User Pages destroy should not allow the admin to delete herself
     Failure/Error: expect { delete :destroy, :id => admin.id }.should_not change(User, :count)
     ArgumentError:
       bad argument (expected URI object or URI string)
     # ./spec/requests/user_pages_spec.rb:180:in `block (4 levels) in <top (required)>'
     # ./spec/requests/user_pages_spec.rb:180:in `block (3 levels) in <top (required)>'

So, my questions are: 1) Why is this code above failing? 2) How do I simulate a "delete" in order to call the destroy action in my controller?

Environment: Mac OSX ruby 1.9.3p194 Rails 3.2.3

Gems for testing:
group :test do gem 'rspec-rails', '2.9.0' gem 'capybara', '1.1.2' gem 'rb-fsevent', '0.4.3.1', :require => false gem 'growl', '1.0.3' gem 'guard-spork', '0.3.2' gem 'spork', '0.9.0' gem 'factory_girl_rails', '1.4.0' end

More Info I have tried a ton of ways to try to simulate clicking on the delete link and none seem to work. I've been using the debugger gem to see if the destroy method is even being called. In the test that clicks on the link to delete a different user, the destroy method gets called and it works fine:

it "should be able to delete another user" do
  expect { click_link('delete') }.to change(User, :count).by(-1)
end

But nothing I have tried to generate the delete request directly has worked to call the destroy method.

Thanks for your help!

Will

** UPDATE **

I tried DVG's suggestion:

describe "destroy" do
    let(:admin) { FactoryGirl.create(:admin) }

    it "should not allow the admin to delete herself" do
      sign_in admin
      #expect { delete user_path(admin), method: :delete }.should change(User, :count)
      expect { delete :destroy, :id => admin }.to_not change(User, :count)
    end
  end

And got this error:

6) User Pages destroy should not allow the admin to delete herself
     Failure/Error: expect { delete :destroy, :id => admin }.to_not change(User, :count)
     ArgumentError:
       bad argument (expected URI object or URI string)
     # ./spec/requests/user_pages_spec.rb:190:in `block (4 levels) in <top (required)>'
     # ./spec/requests/user_pages_spec.rb:190:in `block (3 levels) in <top (required)>'

SOLUTION

I figured it out after FOREVER.

I had to use Rack::Test to issue the DELETE request, but Capybara and Rack::Test don't share the same MockSession, so I had to pull in the :remember_token and :!sample_app_session cookies and put them into the DELETE request manually. Here is what worked. (the other problem I was having, listed below, was that I had a force_ssl statement that was not letting my destroy action get called.

describe "destroy" do
    let!(:admin) { FactoryGirl.create(:admin) }

    before do
      sign_in admin
    end

    it "should delete a normal user" do
      user = FactoryGirl.create(:user)
      expect { delete user_path(user), {},
       'HTTP_COOKIE' => "remember_token=#{admin.remember_token},
        #{Capybara.current_session.driver.response.headers["Set-Cookie"]}" }.
        to change(User, :count).by(-1)
    end

    it "should not allow the admin to delete herself" do
      expect { delete user_path(admin), {},
       'HTTP_COOKIE' => "remember_token=#{admin.remember_token},
        #{Capybara.current_session.driver.response.headers["Set-Cookie"]}" }.
       to_not change(User, :count)
    end
  end

I had a force_ssl statement after my before_filters in my users_controller.rb and this was somehow throwing things off so I never got to the destroy action.

class UsersController < ApplicationController
  before_filter :signed_in_user,  only: [:edit, :update, :index]
  before_filter :existing_user,   only: [:new, :create]
  before_filter :correct_user,    only: [:edit, :update]
  before_filter :admin_user,      only: :destroy

  #force_ssl

  def index
    @users = User.paginate(page: params[:page])
  end

  def show 
    @user = User.find(params[:id])
    @microposts = @user.microposts.paginate(page: params[:page])
  end

  def destroy
    @user = User.find(params[:id])
    if current_user?(@user)
      redirect_to users_path, notice: "You can't destroy yourself."
    else
      @user.destroy
      flash[:success] = "User destroyed."
      redirect_to users_path
    end
  end

These were helpful in getting to a solution

https://gist.github.com/484787

http://collectiveidea.com/blog/archives/2012/01/05/capybara-cucumber-and-how-the-cookie-crumbles/

回答1:

You are confusing rspec-rails request specs which are integration tests and are executed in a simulated browser and controller specs which test controller in isolation. delete(action, *args) (and get, post and so on) - is a method simulating request from ActionController::TestCase, so it's not available in your test.

So your only option is to simulate a click in a browser. I don't know how you hide your delete link, if the html is there but hidden you should be able to click it. If it's not there (removed on the server side when generating view) you can use capybara's page.execute_script (but you have to enable javascript for this example :js => true). You can either add the link back:

page.execute_script("$('body').append("<a href="/users/1" data-method="delete" rel="nofollow">Destroy</a>")")

or make ajax call:

page.execute_script("$.ajax({type:'DELETE',url:'/users/1'})")

Didn't test but something like this should work.



回答2:

I solved this same problem using the following:

describe "should not be able to delete themselves" do
  it { expect { delete user_path(admin) }.not_to change(User, :count) }
end


回答3:

CallumD's solution worked for me, and seemed the most consistent with the techniques recommended in the rest of Michael Hartl's tutorial. But I wanted to tighten up the syntax a little to make it more consistent with the other specs in the same tutorial:

it "should not be able to delete itself" do
  expect { delete user_path(admin) }.not_to change(User, :count)
end


回答4:

This is what I ended up with (Rspec 3.2):

describe 'DELETE destroy' do
  before :each do
    delete :destroy, { id: current_partner_role }
  end

  it 'destroys role' do
    expect(assigns(:role).destroyed?).to be true
  end

"destroyed?" method itself is spec-ed by Rails so IMHO it should be ok to rely on it.

https://github.com/rails/rails/blob/5142d5411481c893f817c1431b0869be3745060f/activerecord/lib/active_record/persistence.rb#L91



回答5:

Try this:

expect { delete :destroy, :id => admin }.to_not change(User, :count)