DatabaseCleaner / database_cleaner-active_record

Strategies for cleaning databases using ActiveRecord. Can be used to ensure a clean state for testing.
MIT License
64 stars 63 forks source link

Clean with transaction not working for 2 databases #10

Open clemens opened 7 years ago

clemens commented 7 years ago

I've used DatabaseCleaner for several years (thanks!) without issues. But my current setup seems to not work properly.

Basic setup:

On noteworthy thing about the app here is that it's using multiple databases: One main database and several tenant databases. In the test environment, this currently means two databases in total (main + test tenant).

Here's the test setup:

TEST_TENANT = 'the_tenant'
TEST_TENANT_API_KEY = SecureRandom.uuid.gsub('-', '')

RSpec.configure do |config|
  config.mock_with :rspec
  config.expect_with :rspec
  config.raise_errors_for_deprecations!
  # config.order = 'random'

  config.before(:suite) do
    ApiApp.establish_connection # ApiApp is connected to the main DB

    primary_cleaner.clean_with(:truncation)

    tenant = ApiApp.where(identifier: TEST_TENANT).first_or_create!(
      name: 'The tenant',
      api_key: TEST_TENANT_API_KEY
    )
    # force full recreate of tenant DB
    config = ActiveRecord::Base.configurations[tenant.identifier]
    ActiveRecord::Tasks::DatabaseTasks.drop(config) rescue nil
    ActiveRecord::Tasks::DatabaseTasks.create(config)
    ActiveRecord::Tasks::DatabaseTasks.load_schema(config, :sql, 'db/structure.sql')

    # seed it
    AppSetter.with(TEST_TENANT) do
      Dir["#{Dir.pwd}/spec/seeds/*.rb"].sort.each { |f| load f }
    end
  end

  config.before(:each) do |example|
    AppSetter.set(TEST_TENANT)

    puts ['[RSpec] start before', "number of patients: #{Patient.count}", Patient.connection.instance_variable_get(:@config)[:database]].inspect
    # primary_cleaner.start
    tenant_cleaner.start
    puts ['[RSpec] end before', "number of patients: #{Patient.count}", Patient.connection.instance_variable_get(:@config)[:database]].inspect
  end

  config.append_after(:each) do
    puts ['[RSpec] start append_after', "number of patients: #{Patient.count}", Patient.connection.instance_variable_get(:@config)[:database]].inspect
    # primary_cleaner.clean
    tenant_cleaner.clean
    puts ['[RSpec] end append_after', "number of patients: #{Patient.count}", Patient.connection.instance_variable_get(:@config)[:database]].inspect
    puts '-' * 100
  end

  private

  def primary_cleaner
    @primary_cleaner ||= cleaner_for(connection: ENV['RACK_ENV'].to_sym)
  end

  def tenant_cleaner
    @tenant_cleaner ||= cleaner_for(connection: TEST_TENANT.to_sym)
  end

  def cleaner_for(options)
    DatabaseCleaner[:active_record, options].tap { |cleaner| cleaner.strategy = :transaction }
  end
end

Now when I have 2 examples in RSpec which both use the same basic setup with a test patient (Patient.create!(email: 'john@doe.com', ...)), the uniqueness constraint on email fails because apparently the record exists already. The debugging code in the output (from the puts statements above) confirms this:

(1) ["[RSpec] start before", "number of patients: 0", "api_test_the_tenant"]
["[DatabaseCleaner] before start", "open transactions: 0", "api_test_the_tenant"]
(2) ["[DatabaseCleaner] after start", "open transactions: 1", "api_test_the_tenant"]
["[RSpec] end before", "number of patients: 0", "api_test_the_tenant"]
(3) ["[RSpec] start append_after", "number of patients: 1", "api_test_the_tenant"]
["[DatabaseCleaner] before clean", "open transactions: 1", "api_test_the_tenant"]
(4) ["[DatabaseCleaner] after clean", "open transactions: 0", "api_test_the_tenant"]
(5) ["[RSpec] end append_after", "number of patients: 1", "api_test_the_tenant"]
----------------------------------------------------------------------------------------------------
(6) ["[RSpec] start before", "number of patients: 1", "api_test_the_tenant"]
["[DatabaseCleaner] before start", "open transactions: 0", "api_test_the_tenant"]
["[DatabaseCleaner] after start", "open transactions: 1", "api_test_the_tenant"]
["[RSpec] end before", "number of patients: 1", "api_test_the_tenant"]
["[RSpec] start append_after", "number of patients: 1", "api_test_the_tenant"]
["[DatabaseCleaner] before clean", "open transactions: 1", "api_test_the_tenant"]
["[DatabaseCleaner] after clean", "open transactions: 0", "api_test_the_tenant"]
["[RSpec] end append_after", "number of patients: 1", "api_test_the_tenant"]
----------------------------------------------------------------------------------------------------

(I've numbered some lines to make it easier to follow. The [DatabaseCleaner] output comes from some puts statements I've put in the corresponding strategy in https://github.com/DatabaseCleaner/database_cleaner/blob/master/lib/database_cleaner/active_record/transaction.rb.)

(1) Before the first example (in the first before block), there are 0 patients in the tenant database => expected. (2) tenant_cleaner.start has caused an open transaction in the tenant database => expected. (3) Running the example has created a patient in the tenant database => expected. (4) tenant_cleaner.clean rolls back the transaction => expected. (5) There is still 1 patient in the tenant database => this is NOT expected. (6) The before block of the next example confirms that 1 patient has remained in the tenant database => this is NOT expected.

Commenting in/out the cleaning of the primary database doesn't change anything (I wouldn't have expected it to either, but I tried it nonetheless).

Does anyone have any insights as to what's going wrong here?

Thanks a lot in advance.

principat commented 7 years ago

i have the same issue in jruby 9.1.12.0-p0: database_cleaner (1.6.1) rspec-core (3.6.0) activerecord (4.2.0) activerecord-jdbc-adapter (1.3.23) activerecord-oracle_enhanced-adapter (1.6.9)

etagwerker commented 7 years ago

@clemens Hi, just curious, why are these lines commented out?

# primary_cleaner.start
# primary_cleaner.clean
clemens commented 7 years ago

During my testing, I cared about the tenant database, not the primary one. In my actual test suite, obviously both should be cleaned.