【问题标题】:RSpec with multi tenancy - Why is this simple test failing?多租户的 RSpec - 为什么这个简单的测试失败了?
【发布时间】:2015-03-27 05:15:41
【问题描述】:

我在做什么

我最近按照Multitenancy with Scopes(需要订阅)作为指导实施了多租户(使用范围)。注意:我 am 使用可怕的“default_scope”来确定租户范围(如 Ryan 的 Railscast 中所示)。在浏览器中一切正常,但我的许多(不是全部)测试都失败了,我不知道为什么。

我从头开始构建身份验证(基于此 Railscast:Authentication from Scratch (revised) - 需要订阅)并使用 auth_token 实现“记住我”功能(基于此 Railscast:Remember Me & Reset Password)。

我的问题

为什么这个测试会失败,为什么这两种变通方法有效?我已经被难住了几天了,就是想不通。

我认为正在发生的事情

我正在调用 Jobs#create 操作,并且 Job.count 减少 1,而不是 增加 1。我认为正在发生的事情是正在创建,然后应用程序将丢失“租户”分配(租户降至零),并且测试正在为错误的租户计算作业。

奇怪的是它期待“1”并得到“-1”(而不是“0”),这意味着它正在获得一个计数(请注意,在 before 块中已经创建了一个“种子”作业,所以它是可能在调用 #create 之前计数“1”),调用创建操作(应该将计数总共增加 1 到 2),然后失去租户并切换到有 0 个工作的 nil 租户.所以它:

  • 计数 1(种子作业)
  • 创建作业
  • 失去租户
  • 在新(可能为零)租户中统计 0 个工作

...导致 Job.count 发生 -1 变化。

您可以在下面看到,我通过在测试中的 Job.count 行中添加“.unscoped”来半确认这一点。这意味着存在预期的作业数量,但这些作业不在应用正在测试的租户中。

我不明白它是如何失去租户的。

代码

我已尝试获取我的代码的相关部分,并创建了一个专用的单一测试规范,以使其尽可能易于剖析。如果我可以做任何其他事情来简化可能的回答者,请告诉我该怎么做!

# application_controller.rb
class ApplicationController < ActionController::Base
  protect_from_forgery
  include SessionsHelper

  around_filter :scope_current_tenant

  private

  def current_user
    @current_user ||= User.unscoped.find_by_auth_token!(cookies[:auth_token]) if cookies[:auth_token]
  end
  helper_method :current_user

  def current_tenant
    @current_tenant ||= Tenant.find_by_id!(session[:tenant_id]) if session[:tenant_id]
  end
  helper_method :current_tenant

  def update_current_tenant
    Tenant.current_id = current_tenant.id if current_tenant
  end
  helper_method :set_current_tenant

  def scope_current_tenant
    update_current_tenant
    yield
  ensure
    Tenant.current_id = nil
  end
end

# sessions_controller.rb
class SessionsController < ApplicationController
  def create
    user = User.unscoped.authenticate(params[:session][:email], params[:session][:password])

    if user && user.active? && user.active_tenants.any?
      if params[:remember_me]
        cookies.permanent[:auth_token] = user.auth_token
      else
        cookies[:auth_token] = user.auth_token
      end

      if !user.default_tenant_id.nil? && (default_tenant = Tenant.find(user.default_tenant_id)) && default_tenant.active
        # The user has a default tenant set, and that tenant is active
        session[:tenant_id] = default_tenant.id
      else
        # The user doesn't have a default
        session[:tenant_id] = user.active_tenants.first.id
      end
      redirect_back_or  root_path
    else
      flash.now[:error] = "Invalid email/password combination."
      @title = "Sign in"
      render 'new'
    end  
  end

  def destroy
    cookies.delete(:auth_token)
    session[:tenant_id] = nil
    redirect_to root_path
  end
end

# jobs_controller.rb
class JobsController < ApplicationController
  before_filter :authenticate_admin

  # POST /jobs
  # POST /jobs.json
  def create    
    @job = Job.new(params[:job])
    @job.creator = current_user

    respond_to do |format|
      if @job.save
        format.html { redirect_to @job, notice: 'Job successfully created.' }
        format.json { render json: @job, status: :created, location: @job }
      else
        flash.now[:error] = 'There was a problem creating the Job.'
        format.html { render action: "new" }
        format.json { render json: @job.errors, status: :unprocessable_entity }
      end
    end
  end
end

# job.rb
class Job < ActiveRecord::Base
  has_ancestry

  default_scope { where(tenant_id: Tenant.current_id) }
  .
  .
  .

end

# sessions_helper.rb
module SessionsHelper

require 'bcrypt'

  def authenticate_admin
    deny_access unless admin_signed_in?
  end

  def deny_access
    store_location
    redirect_to signin_path, :notice => "Please sign in to access this page."
  end

  private

  def store_location
    session[:return_to] = request.fullpath
  end
end

# spec_test_helper.rb
module SpecTestHelper 
  def test_sign_in(user)
    request.cookies[:auth_token] = user.auth_token
    session[:tenant_id] = user.default_tenant_id
    current_user = user
    @current_user = user
  end

  def current_tenant
    @current_tenant ||= Tenant.find_by_id!(session[:tenant_id]) if session[:tenant_id]
  end
end


# test_jobs_controller_spec.rb
require 'spec_helper'

describe JobsController do
  before do
    # This is all just setup to support requirements that the admin is an "Admin" (role)
    # That there's a tenant for him to use
    # That there are some workdays - a basic requirement for the app - jobs, checklist
    # All of this is to satisfy assocations and 
    @role = FactoryGirl.create(:role)
    @role.name = "Admin"
    @role.save
    @tenant1 = FactoryGirl.create(:tenant)
    @tenant2 = FactoryGirl.create(:tenant)
    @tenant3 = FactoryGirl.create(:tenant)

    Tenant.current_id = @tenant1.id
    @user = FactoryGirl.create(:user)
    @workday1 = FactoryGirl.create(:workday)
    @workday1.name = Time.now.to_date.strftime("%A")
    @workday1.save
    @checklist1 = FactoryGirl.create(:checklist)
    @job = FactoryGirl.create(:job)
    @checklist1.jobs << @job
    @workday1.checklists << @checklist1
    @admin1 = FactoryGirl.create(:user)
    @admin1.tenants << @tenant1
    @admin1.roles << @role
    @admin1.default_tenant_id = @tenant1.id
    @admin1.pin = ""
    @admin1.save!
    # This is above in the spec_test_helper.rb code
    test_sign_in(@admin1)
  end

  describe "POST create" do
    context "with valid attributes" do      
      it "creates a new job" do
        expect{ # <-- This is line 33 that's mentioned in the failure below
          post :create, job: FactoryGirl.attributes_for(:job)
        # This will pass if I change the below to Job.unscoped
        # OR it will pass if I add Tenant.current_id = @tenant1.id right here.
        # But I shouldn't need to do either of those because
        # The tenant should be set by the around_filter in application_controller.rb
        # And the default_scope for Job should handle scoping
        }.to change(Job,:count).by(1)
      end
    end
  end
end

这是来自 rspec 的失败:

Failures:

  1) JobsController POST create with valid attributes creates a new job
     Failure/Error: expect{
       count should have been changed by 1, but was changed by -1
     # ./spec/controllers/test_jobs_controller_spec.rb:33:in `block (4 levels) in <top (required)>'

Finished in 0.66481 seconds
1 example, 1 failure

Failed examples:

rspec ./spec/controllers/test_jobs_controller_spec.rb:32 # JobsController POST create with valid attributes creates a new job

如果我添加一些“puts”行来直接查看 current_tenant 是谁,并通过检查会话哈希,我会一直看到相同的租户 ID:

describe "POST create" do
  context "with valid attributes" do      
    it "creates a new job" do
      expect{
        puts current_tenant.id.to_s
        puts session[:tenant_id]
        post :create, job: FactoryGirl.attributes_for(:job)
        puts current_tenant.id.to_s
        puts session[:tenant_id]
      }.to change(Job,:count).by(1)
    end
  end
end

产量...

87
87
87
87
F

Failures:

  1) JobsController POST create with valid attributes creates a new job
     Failure/Error: expect{
       count should have been changed by 1, but was changed by -1
     # ./spec/controllers/test_jobs_controller_spec.rb:33:in `block (4 levels) in <top (required)>'

Finished in 0.66581 seconds
1 example, 1 failure

Failed examples:

rspec ./spec/controllers/test_jobs_controller_spec.rb:32 # JobsController POST create with valid attributes creates a new job

【问题讨论】:

    标签: ruby-on-rails testing rspec multi-tenant default-scope


    【解决方案1】:

    我认为这不是 RSpec 忽略了默认范围,而是通过将当前用户设置为 nil 在周围过滤器中的 ApplicationController 中重置它。

    我在 assigns(...) 中遇到了这个问题,这是因为在您评估分配时关系实际上已解决。我认为您的期望也可能是这种情况。

    更新:在我的情况下,我能找到的最干净的解决方案(尽管我仍然讨厌它)是通过不在测试环境中将当前用户设置为 nil 来让默认范围泄漏。

    在你的情况下,这相当于:

    def scope_current_tenant
      update_current_tenant
      yield
    ensure
      Tenant.current_id = nil unless Rails.env == 'test'
    end
    

    我没有用你的代码测试过,但也许这会有所帮助。

    【讨论】:

    • 这是解决这个问题的一种有趣方式,在应用程序控制器中添加“除非 Rails.env == 'test'”让我非常紧张。我会试一试,看看是否有帮助。谢谢!
    • 实际上我最后也讨厌它,并最终在规范中围绕对 assigns(...) 的调用调用了作用域函数。在您的情况下,这可能类似于controller.scope_current_tenant { ... }
    • 当然,跳过默认作用域并使用显式作用域来限制访问可以让您不用这样的技巧。它可能更乏味,但不太容易出现与 arel 如何处理关系有关的奇怪错误;我的情况比多租户更复杂,但实际上我必须将 Rails 升级到 4.1 才能解决错误。
    • @MarcinBilski 您能否进一步详细说明围绕assigns(...) 调用范围功能?我的scope_current_tenant 函数在ApplicationController 中,但是当我尝试在测试中调用它时,我一直在赌undefined method 错误。
    • controller 在您的测试中可用吗?
    【解决方案2】:

    我设法让我的测试通过了,尽管我仍然不确定为什么他们没有开始。这是我所做的:

      describe "POST create" do
        context "with valid attributes" do      
          it "creates a new job" do
            expect{ # <-- This is line 33 that's mentioned in the failure below
              post :create, job: FactoryGirl.attributes_for(:job)
            }.to change(Job.where(tenant_id: @tenant1.id),:count).by(1)
          end
        end
      end
    

    我变了:

    change(Job,:count).by(1)
    

    ...到:

    change(Job.where(tenant_id: @tenant1.id),:count).by(1)
    

    注意:@tenant1 是登录管理员的租户。

    我假设 default_scopes 会在 RSpec 中应用,但似乎它们不是(或者至少不是在 "expect" 块的 ":change" 部分)。在这种情况下,Job 的 default_scope 是:

    default_scope { where(tenant_id: Tenant.current_id) }
    

    事实上,如果我将该行更改为:

    change(Job.where(tenant_id: Tenant.current_id),:count).by(1)
    

    ...它也会过去的。因此,如果我在规范中明确模仿 Job 的 default_scope,它将通过。这似乎证实了 RSpec 忽略了我在 Jobs 上的 default_scope。

    在某种程度上,我认为我的新测试是确保租户数据保持隔离的更好方法,因为我明确检查特定租户内的计数,而不是隐式检查租户的计数(假设计数在“当前租户”)。

    我将我的答案标记为正确,因为这是唯一的答案,如果其他人遇到此问题,我认为我的答案将帮助他们解决问题。也就是说,我真的没有回答我最初关于为什么测试失败的问题。如果有人对为什么 RSpec 似乎在“期望”块中忽略 default_scope 有任何见解,那可能有助于使这个问题对其他人有用。

    【讨论】:

      【解决方案3】:

      你们也有同样的问题。我没有以一种让我舒服的方式解决,但仍然比验证你的 RAILS_ENV 更好。举个例子吧。

      it "saves person" do
          expect {
            some_post_action
          }.to change(Person, :count).by(1)
        end
      

      每次我尝试保存 count 方法时都会进行如下选择: "从tenant_id为空的人中选择count(*)"

      我设法通过在更改方法中设置 Person.unscoped 来解决这个问题,我改变了这个:

              }.to change(Person, :count).by(1)
      

      到这里:

              }.to change(Person.unscoped, :count).by(1)
      

      这不是最好的解决方案,但我仍在努力寻找绕过 default_scope 的方法。

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2016-10-05
        • 1970-01-01
        • 2021-02-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多