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.