SuperPumpup (dot com)
General awesomeness may be found here.

13 December 2012

PostgreSQL Schema Multi-tenancy and RSpec Testing

I recently struggled with getting a new multitenant application testing well. I used PostgreSQL schema-based multitenancy using the strategy described in Ryan Bates's excellent Railscast (pro). I found a few 'gotchas' and thought I'd share them.

Database Cleanup

config.before(:suite) do

DatabaseCleaner.strategy = :transaction

end

config.before(:each) do

DatabaseCleaner.start

end

config.after(:each) do

Tenant.all.each do |tenant|
  ActiveRecord::Base.connection.execute("DROP SCHEMA tenant#{tenant.id} CASCADE;")
end
DatabaseCleaner.clean

end

Note that if you use this, you shuold turn off the activerecord transactional features by commenting out this line:

config.use_transactional_fixtures = true

or setting it to false. If you are not using transactions in your DatabaseCleaner, AR will sometimes use a different connection from the connection pool before running a code. Practically, this meant to me that some of my FactoryGirl-created objects would sometimes get created in the proper schema, while sometimes they will be in the wrong one.

This leads to my vevy own personal testing hell. Tests occasionally failing. Ouch.

Also, for whatever reason the schemas don't get dropped when DatabaseCleaner's transaction rollback runs, so I have to iterate through tenants and drop the schemas after each test.

Building Your Tests

I set up the following methods in spec_helper.rb to drop into and out of schemas.

def scope_current_tenant(&block)
  return unless @current_tenant
  @current_tenant.scope_schema("public", &block)
end

def scope_to_tenant(tenant)
  tenant.connection.schema_search_path = ["tenant#{tenant.id}","public"].join(",")
  puts "Set the schema_search_path to: #{tenant.connection.schema_search_path}"
end

def reset_scope
  ActiveRecord::Base.connection.schema_search_path = ["public"].join(",")
  puts "Reset schema_search_path to: #{ActiveRecord::Base.connection.schema_search_path}"
end

Practically, here is a controller test:

before :each do
  @tenant = FactoryGirl.create(:tenant)
  @request.host = "#{@tenant.subdomain}.lvh.me"
  scope_to_tenant(@tenant)
  @user = FactoryGirl.create(:user)
  @user.roles << FactoryGirl.create(:role, :name => "admin")
  sign_in @user
end

describe "GET 'show'" do
  describe 'with authorization to retrieve the @user' do
    it "should be successful" do
      get :show, :id => @user.id
      response.should be_success
    end

    it "should find the right user" do
      get :show, :id => @user.id
      assigns(:user).should == @user
    end
  end

And here is a model test:

it "has its roles tables isolated" do
  @tenant = FactoryGirl.create(:tenant)
  scope_to_tenant(@tenant)
  @user = FactoryGirl.create(:user)
  @role = FactoryGirl.create(:role)
  @user.roles << @role
  @role.users.size.should eq 1
  @user.roles.should == [@role]
  @tenant2 = FactoryGirl.create(:tenant)
  Role.all.size.should eq 1
  role_and_user_creation = Proc.new {
    Role.all.size.should eq 0
    user = FactoryGirl.create(:user)
    role = FactoryGirl.create(:role)
    user.roles << role
  }
  @current_tenant = @tenant2
  scope_current_tenant(&role_and_user_creation)
  @role.users.size.should eq 1
end

Hope this helps!

Categories: RSpec Ruby on Rails