← Back to Upcase

How does default_scope work in controller specs?


(Dan Weaver) #1

I’m adding tests to an existing app. The app uses default_scope for multitenancy.

I have a spec file for one controller and the tests are failing due, I think, to default_scope not working as expected during the test. This is a failing test:

require 'spec_helper'

describe LessonsController do
  describe 'GET #index' do
    it 'populates an array of all lessons' do
        lesson1 = create(:lesson)
        lesson2 = create(:lesson)
        sign_in create(:admin)
        get :index
        expect(assigns(:lessons)).to match_array([lesson1, lesson2])
      end
  end
end

My LessonsController action:

def index
  @lessons = Lesson.limit(10)
end

The test is failing because I get an empty array []instead of [lesson1, lesson2]. I’ve checked the test log and I get an empty array because during the query tenant_id is NULL. I can confirm this by adding unscoped to the #index method and the test passes:

def index
  @lessons = Lesson.unscoped.limit(10)
end

The sign_in create(:admin) line in the test should provide a Tenant.current_id and seems to work fine in all my integration tests. I’m using the popular default_scope method outlined by Ryan Bates, with most of my models having the line:

default_scope { where(tenant_id: Tenant.current_id) }

Is there something different about controller specs that means default_scope won’t be triggered? What do I need to do to get past this?


(Derek Prior) #2

Default scope works the same in tests as it does anywhere else. How are you expecting the test to set Tenant.current_id, which is used in that default scope?


(Dan Weaver) #3

@derekprior as per Ryan Bates example, there’s an around filter and a current_tenant method in ApplicationController:

# application_controller.rb
around_filter :scope_current_tenant

def current_tenant
  current_user.tenant
end

def scope_current_tenant
  Tenant.current_id = current_tenant.id
  yield
ensure
  Tenant.current_id = nil
end

This is why I have a user signed in for the tests, in order for there to be a current_tenant.

The Tenant.current_id attribute is defined in the Tenant model:

# tenant.rb
def self.current_id=(id)
  Thread.current[:tenant_id] = id
end
  
def self.current_id
  Thread.current[:tenant_id]
end

I have a spec support file generating a tenant before all tests and I also set the Tenant.current_id explicitly there too:

# spec/support/tenant_builder.rb
RSpec.configure do |config|
  config.before(:all) do
    @main_tenant = Tenant.where(name: 'Main').first_or_create!
    Tenant.current_id = @main_tenant.id
  end
end

Could it be an issue with threads? Or maybe the setting of Tenant.current_id back to nil after each request?


(Dan Weaver) #4

I found some more information here: http://stackoverflow.com/questions/18862567/rspec-with-multi-tenancy-why-is-this-simple-test-failing

It’s true that setting the Tenant.current_id back to nil after each request means that the default_scope isn’t working in the expect(assigns(...)) because this is evaluated after it’s set back to nil.

This results in the condition where(tenant_id: nil) being appended to all queries, which makes them all fail.

As per the SO answer, I can overcome this be adding a condition to the ensure part:

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

This allows the Tenant.current_id to leak through, but it makes me quite uncomfortable to leave this in ApplicationController.

Any suggestions how I can make sure default_scope is used when evaluating assigns(...)?


(Dan Weaver) #5

My current solution is to set the tenant before the expect(...) with a custom helper method like so:

get :index 
set_tenant
expect(assigns(...))

Note that I have to set the tenant after the GET request because it’s during that request that the Tenant is set back to nil.

The helper method is defined in spec/support/controller_helpers.rb:

# spec/support/controller_helpers.rb
module ControllerHelpers
  def set_tenant(tenant = @main_tenant)
    Tenant.current_id = tenant.id
  end
end

which is included with config.include ControllerHelpers, type: :controller.

This also handily means I can switch tenants during a test if I need to:

set_tenant
expect(something for main_tenant to be available)
set_tenant @other_tenant
expect(something for other_tenant)

I create @main_tenant and @other_tenant in an Rspec config.before (:all) block because they’re used so much in the tests.