Isolating tests with temporary ActiveRecord models

546 views
Skip to first unread message

Joseph Haig

unread,
Jul 27, 2021, 10:56:38 AM7/27/21
to rs...@googlegroups.com
I am attempting to refactor the tests in the amoeba gem (https://github.com/amoeba-rb/amoeba) with a view to being able to fix some bugs and (possibly) add new features.

The tests require ActiveRecord models with associations (has_one, has_many, etc) and for the existing tests these are defined all together in one place for the whole suite but, for me, this makes it hard to understand what features are tested in each unit test. I would therefore like to create temporary models with each test, and I can do that as follows:

```
# Create the tables in the database
ActiveRecord::Base.connection.drop_table :parents, if_exists: true
ActiveRecord::Base.connection.drop_table :children, if_exists: true
ActiveRecord::Base.connection.create_table :parents
ActiveRecord::Base.connection.create_table :children do |t|
  t.references :parent
end

# Stub models (so that they are discarded after the tests)
stub_const 'Parent', Class.new(ActiveRecord::Base)
stub_const 'Child', Class.new(ActiveRecord::Base)

# Configure the models, as required
Parent.class_eval 'has_one :child'
```

This works OK but I have a problem if I try to use the same temporary model names with a different association type. Here is an example:

```
require 'spec_helper'

RSpec.describe 'testing' do
  before do
    ActiveRecord::Base.connection.drop_table :parents, if_exists: true
    ActiveRecord::Base.connection.drop_table :children, if_exists: true
    ActiveRecord::Base.connection.create_table :parents
    ActiveRecord::Base.connection.create_table :children do |t|
      t.references :parent
    end

    stub_const 'Parent', Class.new(ActiveRecord::Base)
    stub_const 'Child', Class.new(ActiveRecord::Base)
  end

  describe 'has_one' do
    subject(:record) { Parent.create(child: Child.new) }

    before do
      Parent.class_eval 'has_one :child, inverse_of: :parent'
      Child.class_eval 'belongs_to :parent, inverse_of: :child'
    end

    it { expect { record }.not_to raise_error }
  end

  describe 'has_many' do
    subject(:record) { Parent.create(children: [Child.new]) }

    before do
      Parent.class_eval 'has_many :children, inverse_of: :parent'
      Child.class_eval 'belongs_to :parent, inverse_of: :children'
    end

    it { expect { record }.not_to raise_error }
  end
end
```

The Parent model in the first case has a 'has_one' association and in the second case there is a 'has_many' association. The test in each case checks that an instance can be created. When running each individually (`rspec spec/test_spec.rb:24` and `rspec spec/test_spec.rb:35`) they both pass but when run together I get:

expected no Exception, got #<ActiveRecord::InverseOfAssociationNotFoundError: Could not find the inverse association for parent (:children in Parent)> with backtrace:

with the second test. This suggests to me that the tests are not correctly isolated, and the configuration of the first is affecting the second. I had hoped that the stubbed constants for the models would ensure that all configuration is discarded but is there some sort of cache in ActiveRecord that needs to be cleared? Or maybe what I'm doing is crazy?

Thanks,

Joe

Jon Rowe

unread,
Jul 27, 2021, 2:50:35 PM7/27/21
to rs...@googlegroups.com
Hello!

Its likely that you are confusing Rails here, it generally doesn't (pardon the pun) expect models to be dynamically created and thus may well be keeping things around.

I can suggest two alternative strategies, one is set up a temporary set of models for the entire test run that don't change, another is to generate a new rails app each time you need to and shell out to it, we do something similar within rspec-rails to smoke test various integrations.

I can confirm that your constants would be cleared up, but theres no magic within RSpec to ensure there are no references to those classes other than their constants, thats a bit beyond our reach.

Cheers
Jon
--
You received this message because you are subscribed to the Google Groups "rspec" group.
To unsubscribe from this group and stop receiving emails from it, send an email to rspec+un...@googlegroups.com.

Phil Pirozhkov

unread,
Jul 27, 2021, 4:01:57 PM7/27/21
to Jack Royal-Gordon
Hi Joe,

I would suspect that this isn't due to RSpec caching something, but
more likely due to Rails caching relations internally.
Would `ActiveRecord::Base.connection.schema_cache.clear!` in your root
`before` help?

- Phil

josep...@gmail.com

unread,
Jul 27, 2021, 4:31:28 PM7/27/21
to rspec
Thanks, Jon. I'll have a look at what rspec-rails does but I guess I am going to have to set up the set of models for the whole set. I was hoping to avoid this so that the configuration being tested could be kept near the tests but perhaps I am trying to stretch Rails too much.

Regards,

Joe

josep...@gmail.com

unread,
Jul 27, 2021, 4:36:53 PM7/27/21
to rspec
Thanks, Phil. As it happens I am using `ActiveRecord::Base.connection.schema_cache.clear!` although I didn't add it to the example as it appears not to be necessary for Rails 6.1+. It resets the database connection, so a table that is dropped and recreated with different columns is used correctly, but it doesn't do anything about the model as configured in Ruby.

As I said in my reply to Jon, I think I am going to have to go down the route of setting up all the models and database tables in one place rather than having them configured in the setup of each test.

Regards,

Joe

pirj...@gmail.com

unread,
Jul 27, 2021, 4:42:55 PM7/27/21
to rspec
> It resets the database connection, so a table that is dropped and recreated with different columns is used correctly

What do you refer to as "it"?

Setting up all different models is sure an option, but as far as I understand, your aim was to move all the specifics to the specs.

To my best memory, schema cache is orthogonal to the connection and the actual DB schema.

In any case, all the luck with getting to the bottom of this issue. Please keep us posted on your findings.

- Phil

josep...@gmail.com

unread,
Jul 28, 2021, 4:19:55 PM7/28/21
to rspec
On Tuesday, 27 July 2021 at 21:42:55 UTC+1 pirj...@gmail.com wrote:
> It resets the database connection, so a table that is dropped and recreated with different columns is used correctly

What do you refer to as "it"?

"it" is the `schema_cache.clear!`, which allows for a table to be dropped and then recreated with different columns. This is what I am doing here - https://github.com/jrmhaig/amoeba/blob/a7ccfc42e8039239858d95964d252f4103e708ea/spec/lib/amoeba/config_spec.rb#L135-L138 - to run the same tests over fields of different data types.
 
Setting up all different models is sure an option, but as far as I understand, your aim was to move all the specifics to the specs.

Precisely, but I understand that sometimes the ideal isn't possible. I've not given up quite yet though.
 
In any case, all the luck with getting to the bottom of this issue. Please keep us posted on your findings.
 
The plot thickens. I pulled down Rails from Github so I can try poking around the code there and it appears that this problem goes away in Rails 7.0! Maybe a diff between head and the 6.1 branch will show something ...

Regards,

Joe

josep...@gmail.com

unread,
Jul 28, 2021, 5:00:43 PM7/28/21
to rspec
On Wednesday, 28 July 2021 at 21:19:55 UTC+1 josep...@gmail.com wrote:
The plot thickens. I pulled down Rails from Github so I can try poking around the code there and it appears that this problem goes away in Rails 7.0! Maybe a diff between head and the 6.1 branch will show something ...


FYI, this was the commit that fixed it in Rails 7 - https://github.com/rails/rails/commit/14d4edd7c3b06e82e1fcef54fa0b4453315c35fd - and I have found that I can add:

ActiveSupport::Dependencies::Reference.clear!

before the `stub_const` lines to clear all the cached references and the tests now pass when run together. I will need to have something to skip that when running the tests with Rails 7+ but it looks as though this is the solution.

Regards,

Joe

Phil Pirozhkov

unread,
Jul 30, 2021, 1:24:30 PM7/30/21
to Jack Royal-Gordon
Nice! Thanks for sharing
Reply all
Reply to author
Forward
0 new messages