0

根据 Michael Hartl 关于 Ruby on Rails 的书,我似乎无法理解为什么其中一个测试通过而另一个失败,考虑到它们的作用基本相同。这源自UsersController#destroy练习 9.6-9 中的测试。

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

  2. 为什么考试中authentication_pages_spec.rb永远不会通过?不管是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+\-.]+@[a-z\d\-.]+\.[a-z]+\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 响应的后续版本。应该是_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.

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

rspecspec/requests/user_pages_spec.rb尝试删除user

..The current user is: Person 35
Is user admin?: true
..

Finished in 8.16 seconds
4 examples, 0 failures

rspecspec/requests/user_pages_spec.rb尝试删除non_admin

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 <top (required)>'

rspecspec/requests/authentication_pages_spec.rb尝试删除useror non_admin

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 <top (required)>'

我仍然不确定为什么它会执行 3 次,每次执行一次let(...)

最终编辑

将解决方案视为最后一个答案。

4

2 回答 2

1

user在回答您的第一个问题时,和之间的区别在于您在外部块中non-admin登录。如果您随后尝试登录 as失败并且您仍然登录 as ,这将解释您所看到的行为。您还没有提供 的定义,但如果它不能独立于您是否已经登录并导航到登录页面而工作,那么这将解释发生了什么。userdescribeadminuservalid_signin

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

顺便说一句,如果您将 3.2 代码片段与 4.0 代码片段混合在一起,您将会遇到很多问题。

其他人注意:这是Rails 响应的后续版本。应该是_success 永远不会为真

于 2013-09-25T12:17:14.833 回答
0

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

相反,let!(..:)可用于强制其评估。我必须感谢#rubyonrails频道的NemesisD指出这一点,还要感谢 Stackoverflow 上的Peter Alfvin

测试通过的最终代码如下所示(参见从let到 的更改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
于 2013-09-25T19:47:27.817 回答