【问题标题】:Session variables with Cucumber Stories带有 Cucumber Stories 的会话变量
【发布时间】:2009-08-13 12:52:21
【问题描述】:

我正在为一个包含多个步骤的“注册”应用程序编写一些 Cucumber 故事。

与其编写一个 Huuuuuuuge 故事来一次涵盖所有步骤,这将是糟糕,我宁愿像普通用户一样完成控制器中的每个操作。我的问题是我将在第一步中创建的帐户 ID 存储为会话变量,因此当访问第 2 步、第 3 步等时,会加载现有的注册数据。

我知道能够在 RSpec 规范中访问controller.session[..],但是当我尝试在 Cucumber 故事中执行此操作时,它会失败并出现以下错误(而且,我还在某处读过这是一种反模式等...):

使用 controller.session[:whatever] 或 session[:whatever]

You have a nil object when you didn't expect it!
The error occurred while evaluating nil.session (NoMethodError)

使用 session(:whatever)

wrong number of arguments (1 for 0) (ArgumentError)

所以,似乎加入会话存储是不可能的。我想知道是否有可能(我猜哪个是最好的..):

  1. 模拟会话存储等
  2. 在控制器中有一个方法并将其存根(例如,get_registration 分配一个实例变量...)

我浏览了 RSpec 书(好吧,略读)并浏览了 WebRat 等,但我还没有真正找到我的问题的答案...

为了澄清一点,注册过程更像是一个状态机 - 例如。用户在注册完成之前要经过四个步骤 - 因此“登录”并不是一个真正的选择(它破坏了网站的运作模式)......

在我的控制器规范中,我能够取消对基于会话 var 加载模型的方法的调用 - 但我不确定“反模式”行是否也适用于存根和模拟?

谢谢!

【问题讨论】:

    标签: ruby-on-rails testing rspec cucumber webrat


    【解决方案1】:

    我将重复 danpickett 的说法,即在 Cucumber 中应尽可能避免模拟。但是,如果您的应用没有登录页面,或者性能有问题,则可能需要直接模拟登录。

    这是一个丑陋的 hack,但它应该可以完成工作。

    Given /^I am logged in as "(.*)"$/ do |email|
      @current_user = Factory(:user, :email => email)
      cookies[:stub_user_id] = @current_user.id
    end
    
    # in application controller
    class ApplicationController < ActionController::Base
      if Rails.env.test?
        prepend_before_filter :stub_current_user
        def stub_current_user
          session[:user_id] = cookies[:stub_user_id] if cookies[:stub_user_id]
        end
      end
    end
    

    【讨论】:

    • 刚刚使用它来跳过 OAuth 站点的登录。杰出的。谢谢瑞恩。
    【解决方案2】:

    模拟在黄瓜场景中很糟糕 - 它们几乎是一种反模式。

    我的建议是编写一个实际登录用户的步骤。我这样做

    Given I am logged in as "auser@example.com"
    
    Given /^I am logged in as "(.*)"$/ do |email|
      @user = Factory(:user, :email => email)
      @user.activate!
      visit("/session/new")
      fill_in("email", :with => @user.email)
      fill_in("password", :with => @user.password)
      click_button("Sign In")
    end
    

    我意识到实例变量@user 是一种不好的形式——但我认为在登录/注销的情况下,拥有@user 绝对是有帮助的。

    有时我称之为@current_user

    【讨论】:

    • 感谢您的输入...我已经用更多细节更新了我的问题,但是您的建议并没有真正帮助,因为它涉及专门为测试添加功能,这对我来说看起来更像是一种“反模式”——但是,存根控制器方法会违反这条规则吗?
    • 附加功能?用户如何登录您的网站?你提到你的注册过程,但没有登录。为什么你不能为一个已经注册的用户创建一个工厂并像我上面展示的那样让他们登录?黄瓜故事应该准确地表达用户如何与您的网站互动。用户不能存根控制器方法。他们有要填写的表格和要点击的链接。如果不模仿用户行为,就不是验收测试。
    • 我在这里看到了双方。对于测试身份验证按我想要的方式工作的功能,我几乎完全符合上述要求。对于其他功能,必须重复这些 capybara 步骤来登录每个场景似乎很痛苦。 Cucumber 可以为测试目的操作数据库状态,为什么不能在会话中操作?
    • 作为一名开发人员,我知道为什么要避免每一步登录的“额外工作”,感觉很糟糕。但这正是您在测试中想要的:每个步骤都应该 100% 独立于其他步骤。这允许它们一次运行一个,或者以不同的顺序运行,而不必担心每个场景之间的依赖关系。
    【解决方案3】:

    回复。 Ryan 的解决方案 - 您可以在 env.rb 文件中打开 ActionController 并将其放置在那里以避免放入您的生产代码库(感谢 john @pivotal labs)

    # in features/support/env.rb
    class ApplicationController < ActionController::Base
      prepend_before_filter :stub_current_user
      def stub_current_user
        session[:user_id] = cookies[:stub_user_id] if cookies[:stub_user_id]
      end
    end
    

    【讨论】:

    • 我在 Rails 4 中,我观察到这会“阻止”我现有的 before_filter 函数的发生,以及阻止我的 helper_methods 的存在。
    【解决方案4】:

    我不知道这与原来的问题有多大关系,但我还是决定本着讨论的精神发帖......

    我们有一个运行时间超过 10 分钟的黄瓜测试套件,因此我们想做一些优化。在我们的应用中,登录过程会触发许多与大多数场景无关的额外功能,因此我们希望通过直接设置会话用户 ID 来跳过这些功能。

    Ryanb 的上述方法效果很好,只是我们无法使用该方法注销。这让我们的多用户故事失败了。

    我们最终创建了一个仅在测试环境中启用的“快速登录”路由:

    # in routes.rb
    map.connect '/quick_login/:login', :controller => 'logins', :action => 'quick_login'
    

    下面是创建会话变量的对应动作:

    # in logins_controller.rb
    class LoginsController < ApplicationController
      # This is a utility method for selenium/webrat tests to speed up & simplify the process of logging in.
      # Please never make this method usable in production/staging environments.
      def quick_login
        raise "quick login only works in cucumber environment! it's meant for acceptance tests only" unless Rails.env.test?
        u = User.find_by_login(params[:login])
        if u
          session[:user_id] = u.id
          render :text => "assumed identity of #{u.login}"
        else
          raise "failed to assume identity"
        end
      end
    end
    

    对我们来说,这最终比使用 cookies 数组更简单。作为奖励,这种方法也适用于 Selenium/Watir。

    缺点是我们在应用程序中包含了与测试相关的代码。就我个人而言,我不认为添加代码以使应用程序更具可测试性是一个巨大的罪过,即使它确实增加了一些混乱。也许最大的问题是未来的测试作者需要弄清楚他们应该使用哪种类型的登录。有了无限的硬件性能,我们显然不会这样做。

    【讨论】:

    • 有趣的想法,并且可能有一个替代方法可以在您的生产就绪代码中添加测试代码 - 也许当您的测试启动时,您可以注入新路由和 class_eval 应用程序控制器来处理快速登录代码......无论如何只是一个想法;)感谢您的贡献!
    【解决方案5】:

    Re:Ryan 的解决方案:

    不适用于 Capybara,除非进行了小的改编:

    rack_test_driver = Capybara.current_session.driver
    cookie_jar = rack_test_driver.current_session.instance_variable_get(:@rack_mock_session).cookie_jar
    @current_user = Factory(:user)
    cookie_jar[:stub_user_id] = @current_user.id
    

    (在这里找到:https://gist.github.com/484787

    【讨论】:

    • 感谢您再次在这里提供您的解决方案,这对我帮助很大,它就像一个魅力。
    • 对我来说这只是触发错误:undefined method 'current_session' for #&lt;Capybara::RackTest::Driver:0x20ebe28&gt; 您能否更具体地说明您需要将此代码放在应用程序的哪个位置?
    【解决方案6】:

    我的理解是你得到:

    You have a nil object when you didn't expect it!
    The error occurred while evaluating nil.session (NoMethodError)
    

    在请求实例化之前访问 session[] 时。在您的情况下,我想如果您在访问 session[] 之前将 webrats 的 visit some_existing_path 放在您的步骤定义中,错误就会消失。

    现在,不幸的是,会话似乎并没有跨步骤持续存在(至少,我找不到路),所以这些信息无助于回答您的问题 :)

    所以,我想,Ryan 的 session[:user_id] = cookies[:stub_user_id]... 是要走的路。虽然,imo,在应用程序本身中测试相关代码听起来不太对。

    【讨论】:

      【解决方案7】:

      我使用像 Prikka's 这样的仅测试登录解决方案,但我在 Rack 中完成这一切,而不是创建新的控制器和路由。

      # in config/environments/cucumber.rb:
      
      config.middleware.use (Class.new do
        def initialize(app); @app = app; end
        def call(env)
          request = ::Rack::Request.new(env)
          if request.params.has_key?('signed_in_user_id')
            request.session[:current_user_id] = request.params['signed_in_user_id']
          end
          @app.call env
        end
      end)
      
      # in features/step_definitions/authentication_steps.rb:
      Given /^I am signed in as ([^\"]+)$/ do |name|
        user = User.find_by_username(name) || Factory(:user, :username => name)
        sign_in_as user
      end
      
      # in features/step_definitions/authentication_steps.rb:
      Given /^I am not signed in$/ do
        sign_in_as nil
      end
      
      module AuthenticationHelpers
        def sign_in_as(user)
          return if @current_user == user
          @current_user = user
          get '/', { 'signed_in_user_id' => (user ? user.to_param : '') }
        end
      end
      
      World(AuthenticationHelpers)
      

      【讨论】:

        【解决方案8】:

        @Ajedi32 我遇到了同样的问题(Capybara::RackTest::Driver 的未定义方法 'current_session')并将其放在我的步骤定义中为我解决了问题:

        rack_test_browser = Capybara.current_session.driver.browser
        
        cookie_jar = rack_test_browser.current_session.instance_variable_get(:@rack_mock_session).cookie_jar
        cookie_jar[:stub_user_id] = @current_user.id
        

        在我的控制器操作中,我提到了 cookies[:stub_user_id],而不是 cookie_jar[:stub_user_id]

        【讨论】:

          【解决方案9】:

          为什么不将 FactoryGirl 或(Fixjour 或 Fabricator)与 Devise(或 Authlogic)和 SentientUser 一起使用?然后你就可以简单地嗅探哪个用户已经登录了!

          @user = Factory(:user)       # FactoryGirl
          sign_in @user                # Devise
          User.current.should == @user # SentientUser
          

          【讨论】:

            【解决方案10】:

            另一个细微的变化:

            # In features/step_definitions/authentication_steps.rb:
            
            class SessionsController < ApplicationController
              def create_with_security_bypass
                if params.has_key? :user_id
                  session[:user_id] = params[:user_id]
                  redirect_to :root
                else
                  create_without_security_bypass
                end
              end
            
              alias_method_chain :create, :security_bypass
            end
            
            Given %r/^I am logged in as "([^"]*)"$/ do |username|
              user = User.find_by_username(username) || Factory(:user, :username => username)
              page.driver.post "/session?user_id=#{user.id}"
            end
            

            【讨论】:

            • 我收到这个错误! undefined method 'post' for #&lt;Capybara::Selenium::Driver:0x42ad1b4&gt; (NoMethodError)。看起来我们至少不能用 Selenium 发帖。
            【解决方案11】:

            经过大量的灵魂搜索和网上冲浪,我最终选择了一个非常简单明了的解决方案。

            使用 cookie 会增加两个问题。首先,您在应用程序中有专门用于测试的代码,其次存在一个问题,即在 Cucumber 中创建 cookie 在使用机架测试以外的任何东西时都很困难。 cookie 问题有多种解决方案,但都具有一定的挑战性,有些引入了模拟,而且所有这些都是我所说的“棘手”。一种这样的解决方案是here

            我的解决方案如下。这是使用 HTTP 基本身份验证,但它可以推广到大多数情况。

              authenticate_or_request_with_http_basic "My Authentication" do |user_name, password|
                if Rails.env.test? && user_name == 'testuser'
                  test_authenticate(user_name, password)
                else
                  normal_authentication
                end
              end
            

            test_authenticate 做普通认证所做的一切,除了它绕过任何耗时的部分。就我而言,真正的身份验证是使用我想避免的 LDAP。

            是的……这有点粗俗,但清晰、简单、明显。而且……我见过的其他解决方案都没有比这更干净或更清晰了。

            注意,一个特点是,如果 user_name 不是 'testuser',则采用正常路径,以便对其进行测试。

            希望这对其他人有所帮助...

            【讨论】:

              猜你喜欢
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 1970-01-01
              • 2016-11-11
              • 2011-11-01
              • 2014-06-30
              • 1970-01-01
              • 1970-01-01
              相关资源
              最近更新 更多