用于测试DELETE请求的Rspec类似代码给出了不同的结果

继迈克尔哈特尔关于Ruby on Rails的书之后,我似乎无法理解为什么其中一个测试通过而另一个测试失败,考虑到它们大致相同。 这源于在练习9.6-9中测试UsersController#destroy

这是我的spec/requests/user_pages_spec.rb

 require 'spec_helper' describe "User pages" do subject { page } describe "index" do let(:user){ FactoryGirl.create(:user) } #before/after ALL TESTS, not USERS before(:all){ 30.times {FactoryGirl.create(:user) }} after(:all) {User.delete_all} #before EACH TEST, no user before(:each) do valid_signin user visit users_path end describe "delete links" do it { should_not have_link('delete') } describe "as an admin user" do let(:admin) { FactoryGirl.create(:admin) } let(:non_admin) { FactoryGirl.create(:user) } before do valid_signin admin visit users_path end it "should be able to delete another user" do expect { delete user_path(user) }.to change(User, :count).by(-1) end . . . end 

这是我的spec/requests/authentication_pages_spec.rb

需要’spec_helper’

 describe "AuthenticationPages" do subject{ page } describe "signin page" do before { visit signin_path } it { should have_selector('h1',text: 'Sign in') } it { should have_selector('title', text: full_title('Sign in')) } end describe "signin" do before { visit signin_path } #INVALID INFO describe "with invalid information" do before { click_button "Sign in"} it{ should have_selector('title', text: 'Sign in')} #it{ should have_selector('div.alert.alert-error', text: 'Invalid')} it{ should have_error_message('Invalid')} describe "after visiting another page" do before { click_link "Home" } #it{ should_not have_selector('div.alert.alert-error')} it{ should_not have_error_message()} #exercise 9.6-3 it{ should_not have_link('Profile')} it{ should_not have_link('Settings')} end end #VALID INFO describe "with valid information" do let(:user) { FactoryGirl.create(:user) } before{ valid_signin(user) } it{ should have_selector('title', text: user.name)} it{ should have_link('Users', href: users_path)} it{ should have_link('Profile', href: user_path(user))} it{should have_link('Settings', href: edit_user_path(user))} it{ should have_link('Sign out', href: signout_path)} it{ should_not have_selector('Sign in', href:signin_path)} describe "followed by signout" do before{click_link "Sign out"} it{ should have_link('Sign in') } end #Exercise 9.6-6 describe "accessing new and create actions" do describe "through website" do before{visit signup_path} it{ should_not have_selector('h1',text:"Sign up")} it{ should_not have_button("Create my account")} end describe "through a POST request" do before { post users_path} specify { response.should redirect_to(root_path)} end end end end describe "authorization" do describe "as non-admin user" do let(:admin) {FactoryGirl.create(:admin)} let(:non_admin) {FactoryGirl.create(:user)} before{valid_signin non_admin} #Check that loggin to nonadmin works(debug ex.9.6-9) describe "should render the non-admin profile page" do it{ should have_selector('title', text: non_admin.name)} end describe "submitting a DELETE request to the Users#destroy action" do before do delete user_path(admin) #puts response.message #puts response.success? end specify{ response.should redirect_to(root_path) } specify{ response.should_not be_success } end end #Exercise 9.6-9 prevent admin from destroying himself describe "as admin user" do let(:user){FactoryGirl.create(:user)} let(:admin){FactoryGirl.create(:admin)} let(:non_admin){FactoryGirl.create(:user)} before do valid_signin admin visit users_path end it "should be able to delete another user" do expect { delete user_path(user) }.to change(User, :count).by(-1) end . . . end 

我知道通过与应用程序交互,删除用户确实有效,问题在于测试它。这里感兴趣的测试是描述为“应该能够删除另一个用户” ,这在两个文件中都是相同的user_pages_spec.rbauthentication_pages_spec.rb

有两件事我似乎无法理解:

  1. user_pages_spec.rb中, expect { delete user_path(user) }.to change(User,:count).by(-1) user_pages_spec.rb expect { delete user_path(user) }.to change(User,:count).by(-1) 确实通过了,但是如果我将其更改为expect { delete user_path(non_admin) }.to change(User,:count).by(-1) ,它失败了。 这是为什么? 它们都使用相同的工厂参数创建。

  2. 为什么authentication_pages_spec.rb的测试从未传递? 无论是user_path(non_admin) user_path(user)还是user_path(non_admin)

这是我的工厂:

 FactoryGirl.define do factory :user do sequence(:name){ |n| "Person #{n}" } sequence(:email){ |n| "person_#{n}@example.com"} password "foobar" password_confirmation "foobar" factory :admin do admin true end end end 

这是我的users_controller.rb

 class UsersController < ApplicationController before_filter :signed_in_user, only: [:index, :edit, :update] before_filter :correct_user, only: [:edit, :update] before_filter :admin_user, only: [:destroy] def new #change for exercise 9.6-6 if signed_in? redirect_to root_path else @user=User.new end end def show @user=User.find(params[:id]) end def create if signed_in? redirect_to root_path else @user = User.new(params[:user]) if @user.save sign_in @user flash[:success]="Welcome to the Sample App!" # Handle a successful save. redirect_to @user else render 'new' end end end def edit #@user= User.find(params[:id]) <----we can delete this because the before filter correct_user now defines @user variable end def update #@user = User.find(params[:id]) if @user.update_attributes(params[:user]) # Handle a successful update. flash[:success]="Profile updated" sign_in @user redirect_to @user else render 'edit' end end def index #@users= User.all @users= User.paginate(page: params[:page]) end def destroy puts "The current user is:"+current_user.name puts "Is user admin?:"+current_user.admin.to_s User.find(params[:id]).destroy flash[:success]="User destroyed." redirect_to users_path end private def signed_in_user unless signed_in? store_location redirect_to signin_path, notice: "Please sign in." end end def correct_user @user =User.find(params[:id]) redirect_to(root_path) unless current_user?(@user) end def admin_user redirect_to(root_path) unless current_user.admin? end end 

和模型user.rb

 # == Schema Information # # Table name: users # # id :integer not null, primary key # name :string(255) # email :string(255) # created_at :datetime not null # updated_at :datetime not null # class User < ActiveRecord::Base attr_accessible :email, :name, :password, :password_confirmation has_secure_password before_save { self.email.downcase! } #callback for session token generation before_save :create_remember_token validates :name, presence: true, length: {maximum: 50} VALID_EMAIL_REGEX = /\A[\w+\-.]+@[az\d\-.]+\.[az]+\z/i validates :email, presence: true, format: { with: VALID_EMAIL_REGEX }, uniqueness: { case_sensitive: false} #validates :password, presence: true, length:{minimum:6} validates :password, length:{minimum:6} validates :password_confirmation, presence: true private def create_remember_token self.remember_token=SecureRandom.urlsafe_base64 end end 

这是定义valid_signin的支持文件:

 include ApplicationHelper def valid_signin(user) visit signin_path fill_in "Email", with: user.email fill_in "Password", with: user.password click_button "Sign in" # Sign in when not using Capybara as well. cookies[:remember_token] = user.remember_token end def valid_signup(user) fill_in "Name", with: user.name fill_in "Email", with: user.email fill_in "Password", with: user.password fill_in "Confirm Password", with: user.password_confirmation #click_button "Sign in" end RSpec::Matchers.define :have_error_message do |message| match do |page| page.should have_selector('div.alert.alert-error', text: message) end end RSpec::Matchers.define :have_success_message do |message| match do |page| page.should have_selector('div.alert.alert-success', text: message) end end 

“注意别人:这是Rails响应的后续。应该be_success永远不会成真 ”

编辑

这是SessionsHelper (spec / helpers / sessions_helper.rb):

 module SessionsHelper def sign_in(user) cookies.permanent[:remember_token]=user.remember_token current_user=user end def signed_in? !current_user.nil? end def current_user=(user) @current_user = user end def current_user @current_user ||= User.find_by_remember_token(cookies[:remember_token]) end def current_user?(user) user == current_user end def sign_out current_user=nil cookies.delete(:remember_token) end def redirect_back_or(default) redirect_to(session[:return_to] || default) session.delete(:return_to) end def store_location session[:return_to] = request.fullpath end end 

编辑2添加了putsUsersController#destroy

这是不同测试运行的输出:

尝试删除user spec/requests/user_pages_spec.rb上的rspec:

 ..The current user is: Person 35 Is user admin?: true .. Finished in 8.16 seconds 4 examples, 0 failures 

尝试删除non_admin spec/requests/user_pages_spec.rb上的rspec:

 The current user is: Person 35 Is user admin?: true F. Failures: 1) User pages index delete links as an admin user should be able to delete another user Failure/Error: expect { delete user_path(non_admin) }.to change(User, :count).by(-1) count should have been changed by -1, but was changed by 0 # ./spec/requests/user_pages_spec.rb:48:in `block (5 levels) in ' 

尝试删除usernon_admin spec/requests/authentication_pages_spec.rb上的rspec:

 Run options: include {:full_description=>/(?-mix:as\ admin\ user)/} The current user is: Person 1 Is user admin?: true FThe current user is: Person 3 Is user admin?: true FThe current user is: Person 5 Is user admin?: true . Failures: 1) AuthenticationPages authorization as admin user should be able to delete another user Failure/Error: expect { delete user_path(user) }.to change(User, :count).by(-1) count should have been changed by -1, but was changed by 0 # ./spec/requests/authentication_pages_spec.rb:99:in `block (4 levels) in ' 

我仍然不确定它为什么会这样做三次,每次一次let(...)

最终编辑

请将解决方案作为最后答案

在回答您的第一个问题时, usernon-admin之间的区别在于您在外部describe块中以user身份登录。 如果您之后以admin身份登录的尝试失败并且您仍以user身份登录,则可以解释您所看到的行为。 您尚未提供valid_signin的定义,但如果它不起作用,不管您是否已登录并已导航到登录页面,那么这将解释正在发生的事情。

同样,您的authentication_pages_spec.rb示例完全依赖于valid_signin成功运行。 虽然您之前未在此示例中登录,但您还没有进行任何导航,因此如果valid_signin是一个简单的表单填充(因为它是在http:// ruby的3.2版本教程中定义的) 。 railstutorial.org/book/ruby-on-rails-tutorial?version=3.2 ),这就解释了为什么它在这种情况下失败了。

顺便说一句,如果你将3.2代码片段与4.0代码片段混合使用,那么你将会遇到很多问题。

别人注意:这是Rails响应的后续。应该be_success永远不会成立

好吧,显然我设法解决了这个问题。 问题的根源是let(...)懒惰地评估 ,这意味着“ 直到第一次调用它定义的方法时才会对它进行评估。 ”。 文档在这里 。

相反, let!(..:)可以用来强制进行评估。 我要感谢NemesisD在频道#rubyonrails指出这一点,还有Peter Alfvin在Stackoverflow。

测试传递的最终代码看起来像(请参阅let to let! ):

需要’spec_helper’

 describe "AuthenticationPages" do subject{ page } describe "signin page" do before { visit signin_path } it { should have_selector('h1',text: 'Sign in') } it { should have_selector('title', text: full_title('Sign in')) } end describe "signin" do before { visit signin_path } #INVALID INFO describe "with invalid information" do before { click_button "Sign in"} it{ should have_selector('title', text: 'Sign in')} #it{ should have_selector('div.alert.alert-error', text: 'Invalid')} it{ should have_error_message('Invalid')} describe "after visiting another page" do before { click_link "Home" } #it{ should_not have_selector('div.alert.alert-error')} it{ should_not have_error_message()} #exercise 9.6-3 it{ should_not have_link('Profile')} it{ should_not have_link('Settings')} end end #VALID INFO describe "with valid information" do let(:user) { FactoryGirl.create(:user) } before{ valid_signin(user) } it{ should have_selector('title', text: user.name)} it{ should have_link('Users', href: users_path)} it{ should have_link('Profile', href: user_path(user))} it{should have_link('Settings', href: edit_user_path(user))} it{ should have_link('Sign out', href: signout_path)} it{ should_not have_selector('Sign in', href:signin_path)} describe "followed by signout" do before{click_link "Sign out"} it{ should have_link('Sign in') } end #Exercise 9.6-6 describe "accessing new and create actions" do describe "through website" do before{visit signup_path} it{ should_not have_selector('h1',text:"Sign up")} it{ should_not have_button("Create my account")} end describe "through a POST request" do before { post users_path} specify { response.should redirect_to(root_path)} end end end end describe "authorization" do describe "as non-admin user" do let(:admin) {FactoryGirl.create(:admin)} let(:non_admin) {FactoryGirl.create(:user)} before{valid_signin non_admin} #Check that loggin to nonadmin works(debug ex.9.6-9) describe "should render the non-admin profile page" do it{ should have_selector('title', text: non_admin.name)} end describe "submitting a DELETE request to the Users#destroy action" do before do delete user_path(admin) #puts response.message #puts response.success? end specify{ response.should redirect_to(root_path) } specify{ response.should_not be_success } end end #Exercise 9.6-9 prevent admin from destroying himself describe "as admin user" do let!(:user){FactoryGirl.create(:user)} let!(:admin){FactoryGirl.create(:admin)} let!(:non_admin){FactoryGirl.create(:user)} before{ valid_signin admin } it "should be able to delete another user" do expect { delete user_path(user) }.to change(User, :count).by(-1) end . . . end