1

I'm testing my has_many :through-self-bidirectional relationship with rspec and my joint record simply disappear from a case to another. Of course, I'm not cleaning the db between each, only after all.

author.rb

class Author < ActiveRecord::Base

  has_many :follower_relationships,
    foreign_key: :follower_id,
    class_name: AuthorsRelationship,
    dependent: :destroy
  has_many :followed_relationships,
    foreign_key: :followed_id,
    class_name: AuthorsRelationship,
    dependent: :destroy
  has_many :followers, through: :followed_relationships
  has_many :followeds, through: :follower_relationships

  def follow(followed)
    followeds << followed
    true
  end

  def unfollow(followed)
    !!(follower_relationships.find_by_followed_id(followed).try :destroy)
  end
end

authors_relationship.rb

class AuthorsRelationship < ActiveRecord::Base
  belongs_to :follower, foreign_key: :follower_id, class_name: Author
  belongs_to :followed, foreign_key: :followed_id, class_name: Author

  validate :ensure_different_targets
  validates_uniqueness_of :followed_id,
    scope: :follower_id,
    message: 'is already following the target'

  private

  def ensure_different_targets
    unless follower != followed
      errors.add(:follower_id, "can't be equal to followed_id")
    end
  end
end

author_spec.rb

RSpec.describe Author, type: :model do
  describe Author, '#follow' do

    before :all do
      DatabaseCleaner.start
      @bieber = Author.create!(name: 'Justin Bieber', screen_name: 'justinbieber')
      @teen = Author.create!(name: 'Aya No', screen_name: 'Ayano2327')
    end

    before :each do
      @bieber.reload
      @bieber.followers.reload
      @bieber.followeds.reload

      @teen.reload
      @teen.followers.reload
      @teen.followeds.reload
    end

    after :all do
      DatabaseCleaner.clean
    end

    context 'without followers yet' do
      it 'returns true' do
        result = @teen.follow @bieber
        ap AuthorsRelationship.all
        expect(result).to be true
      end

      it 'should be following after call' do
        ap AuthorsRelationship.all
        expect(@teen.followeds).to eq [@bieber]
      end
    end
  end
end

Second test fails. Here's the output I got from my ap:

[
    [0] #<AuthorsRelationship:0x00000002355f00> {
                 :id => 15,
        :follower_id => 22,
        :followed_id => 21,
         :created_at => Mon, 04 Apr 2016 21:25:04 UTC +00:00,
         :updated_at => Mon, 04 Apr 2016 21:25:04 UTC +00:00
    }
]
.[]

This post rspec testing has_many :through and after_save didn't solve the issue.

Community
  • 1
  • 1
jgburet
  • 535
  • 3
  • 15

2 Answers2

1

before :all isn't reliable with DatabaseCleaner, so it's likely that your data is actually being wiped between each test.

Also, this is an example of why keeping your tests fully independent from one another is a good practice. A given test case should not rely on the state or output of any other test in the suite, which is not the case here. Also remember that RSpec tests can run in random order. So even if your code worked as you expected it to, it may not pass every time.

I would replace the original before :all with before :each and extract the @teen.follow(@bieber) line to a before :each, which should resolve the issue. Also check your configuration. Are you using transactional fixtures? What does your database cleaner configuration look like in your spec_helper file?

EDIT

Per your comment, I'd recommend cleaning your database between tests with database transactions:

I've found the following configuration to work for me:

config.before(:suite) do
  DatabaseCleaner.clean_with :truncation
end

config.after(:suite) do
  DatabaseCleaner.clean_with :truncation
end

config.after(:all, type: :feature) do |example|
  DatabaseCleaner.clean_with :truncation
end

config.before(:each) do |example|
  DatabaseCleaner.strategy = if example.metadata[:js]
                                 :truncation
                               else
                                 :transaction
                               end

  DatabaseCleaner.start
end

config.after(:each) do
  DatabaseCleaner.clean
end
Anthony E
  • 11,072
  • 2
  • 24
  • 44
  • Here's my spec_helper.rb: config.before(:suite) do DatabaseCleaner.strategy = :transaction DatabaseCleaner.clean_with(:deletion) # clean suite now up-front end – jgburet Apr 04 '16 at 22:11
  • If I remove everything about DatabaseCleaner, drop my base and run the test, it still fails the same way. – jgburet Apr 04 '16 at 22:14
  • Which makes sense. The problem isn't with database cleaner. It looks like the following relationship is never actually set in your second test. What does adding `@teen.follow @bieber` to the beginning of your second test give you? – Anthony E Apr 04 '16 at 22:18
  • It make it works when I do so. As mentioned the "joint record simply disappear from a case to another". – jgburet Apr 04 '16 at 22:20
  • That makes sense database is usually cleaned between examples. Do you have `config.use_transactional_fixtures = true` in your config? Again, I'd recommend running tests completely in isolation. – Anthony E Apr 04 '16 at 22:23
  • Nope, I don't. Should I here? – jgburet Apr 04 '16 at 22:25
  • No, I'd make sure `config.use_transactional_fixtures = false` and your database cleaner config looks something like what I posted above. Finally, use `before :each` and set the following relationship there. – Anthony E Apr 04 '16 at 22:27
1

So according to Anthony, I separated my tests. I'm just disappointed by that behavior. Here's my code now :

RSpec.describe Author, type: :model do
  describe Author, '#follow' do

    before :each do
      @bieber = Author.create!(name: 'Justin Bieber', screen_name: 'justinbieber')
      @teen = Author.create!(name: 'Aya No', screen_name: 'Ayano2327')
    end

    context 'when already following' do
      before :each do
        @teen.follow @bieber
      end

      it 'should raise when called' do
        expect { @teen.follow @bieber }.to raise_error(ActiveRecord::RecordInvalid)
      end

      it 'should have made the teen follow bieber' do
        expect(@teen.followeds).to eq [@bieber]
      end

      it 'should have made bieber followed by the teen' do
        expect(@bieber.followers).to eq [@teen]
      end
    end
  end
end
jgburet
  • 535
  • 3
  • 15