Search code examples
ruby-on-railsrspec

Should we test rails attributes?


In Rails models we usually have attributes and relations tests, like:

describe 'attributes' do
  it { is_expected.to have_db_column(:identifier).of_type(:uuid) }
  it { is_expected.to have_db_column(:content).of_type(:jsonb) }
  it { is_expected.to have_db_column(:created_at).of_type(:datetime) }
end

describe 'relations' do
  it { is_expected.to belong_to(:user).class_name('User') }
end

And using a TDD style it seems to be some useful tests, however I have been dwelling if these are really necessary tests, and I would like to know if there is some common knowledge about it, is it good practice to create these tests? or are we just testing rails?


Solution

  • Amongst the purposes of a unit test are...

    • Does it work?
    • Does it still work?

    If it's a promise, if other things rely on it, you should test it to ensure you keep that promise. This is regression testing.

    But don't test more than you promise. You'll be stuck with it, or your code will break when you make an internal change.

    For example...

      it { is_expected.to have_db_column(:identifier).of_type(:uuid) }
    

    This promises that it has a column called identifier which is a UUID. Usually you don't promise all that detail; it is glass-box testing and it makes your test brittle.

    Instead, promise as little as you can. Its ID is a UUID. This is black-box testing.

    require "rspec/uuid"
    
    describe '#id' do
      subject { thing.id }
      let(:thing) { create(:thing) }
    
      it 'has a uuid ID' do
        expect(thing.id).to be_a_uuid
      end
    end
    

    It's possible there is an even higher level way to express this without holding yourself specifically to a UUID.


      it { is_expected.to have_db_column(:content).of_type(:jsonb) }
    

    Similarly, don't promise it has a jsonb column. That is blackbox testing. Promise that you can store complex data structures.

    describe '#content' do
      subject { create(:thing) }
    
      it 'can round trip complex data' do
        data = [1, { two: 3, four: [5] }]
    
        thing.update!(content: data)
    
        # Force it to re-load content from the database.
        thing.reload
    
        expect(thing.content).to eq data
      end
    end
    

      it { is_expected.to belong_to(:user).class_name('User') }
    

    Instead of promising what it belongs to, promise the relationship.

    describe '#user' do
      let(:thing) { create(:thing) }
      let(:user) { create(:user) }
    
      before {
        user.things << thing
      }
    
      it 'belongs to a user' do
        expect(thing.user).to eq user
        expect(user.things).to contain(thing)
      end
    end