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?
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?
@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?
I found some more information here: ruby on rails - RSpec with multi tenancy - Why is this simple test failing? - Stack Overflow
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(...)
?
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.