0

I have 2 model: Answer and Book.

Although they are very similar, all tests pass to Answer but 1 test fails for Book

models/answer.rb

class Answer < ApplicationRecord
  belongs_to :question, class_name: "Question", foreign_key: "question_id"

  validates :text, presence: true
  validates :correct, presence: true
  validates :question_id, presence: true
end

models/book.rb

class Book < ApplicationRecord
  belongs_to :grade, class_name: "Grade", foreign_key: "grade_id"

  validates :name, presence: true, uniqueness: { case_sensitive: false }
  validates :grade_id, presence: true  
end

spec/factories/answers.rb

FactoryBot.define do
  factory :answer do
    text { Faker::Book.author }
    correct { %i(false, true).sample }
    question
  end
end

spec/factories/books.rb

FactoryBot.define do
  factory :book do
    name { Faker::Book.title }
    grade
  end
end

spect/models/answer_spec.rb

require 'rails_helper'

RSpec.describe Answer, type: :model do

  describe 'validation' do
    it { should validate_presence_of(:text) }
    it { should validate_presence_of(:correct) }
    it { should validate_presence_of(:question_id) }
    it { should belong_to :question }
  end
end

spect/models/book_spec.rb

require 'rails_helper'

RSpec.describe Book, type: :model do

  describe 'validation' do
    it { should validate_presence_of(:name) }
    it { should validate_uniqueness_of(:name).case_insensitive }
    it { should validate_presence_of(:grade_id) }
    it { should belong_to :grade }
  end
end

The message I get in return is:

  1. Book validation is expected to validate that :name is case-insensitively unique Failure/Error: it { should validate_uniqueness_of(:name).case_insensitive }

    Shoulda::Matchers::ActiveRecord::ValidateUniquenessOfMatcher::ExistingRecordInvalid: validate_uniqueness_of works by matching a new record against an existing record. If there is no existing record, it will create one using the record you provide.

    While doing this, the following error was raised:
    
      **PG::NotNullViolation: ERROR:  null value in column "grade_id" violates not-null constraint**
      DETAIL:  Failing row contains (2, null, null, 2021-12-23 15:03:39.977355, 2021-12-23 15:03:39.977355).
    
    The best way to fix this is to provide the matcher with a record where
    any required attributes are filled in with valid values beforehand.
    

I have tried many ways that I came up with or saw in stack/git posts but none seams to work.

Wagner Braga
  • 459
  • 4
  • 9
  • 1
    Do you have a factory for grades? – Sebastián Palma Dec 23 '21 at 16:48
  • Yes, I do. I should have posted it to. However y may have found the propper answer. Thanks for care about. – Wagner Braga Dec 23 '21 at 16:53
  • 1
    Side note, your class_name and foreign_key arguments are redundant; they are the defaults. So is the presence check on the foreign key, belongs_to does that. Checking the foreign key in your tests pierces the relationship. Check question and grade, not question_id and grade_id. – Schwern Dec 23 '21 at 16:58
  • Could I leave jut as belongs_to :question or better belongs_to :question, foreign_key: "question_id" ??? – Wagner Braga Dec 23 '21 at 17:02
  • 1
    You've defined the books factory as `factory :book do name { Faker::Book.title } grade end`, it'd be good to know how the grade factory is defined to see why you can not create two records given that definition. Another way to see what happens is to debug an attempt to create two records in a separate `it` block. – Sebastián Palma Dec 23 '21 at 17:13
  • @wagnerbraga `belongs_to : question`. It will figure out the foreign key and class from the name: question_id and Question. Rails refers to this as [Convention Over Configuration](https://guides.rubyonrails.org/active_record_basics.html#convention-over-configuration-in-active-record). – Schwern Dec 23 '21 at 22:03

2 Answers2

2

When trying test uniqueness in a model that has a reference you need to provide one to be compare first.

For doing so you should specify the subject:

 describe 'validation' do
    subject{ 
    }

After that you can call FactoryBot to build the one to be compere to:

FactoryBot.build(:book)

Final snipet:

  describe 'validation' do
    subject{ 
     FactoryBot.build(:book)
    }

Mr. [Schwern][1] Enhanced the code a little better:

describe 'validation' do
  subject{ build(:book)}
Wagner Braga
  • 459
  • 4
  • 9
2

This situation is covered in the validates_uniqueness_of docs.

If there is no existing record, it will create one using the record you provide.

Your spec doesn't say how to create a Book, and it doesn't know that it needs a Grade. You need to tell it, it won't use the factory. Do this by adding a subject for your one-liners to use.

require 'rails_helper'

RSpec.describe Book, type: :model do
  subject { build(:book) }

  describe 'validation' do
    it { should validate_presence_of(:name) }
    it { should validate_uniqueness_of(:name).case_insensitive }
    it { should belong_to :grade }
  end
end

Note that it { should validate_presence_of(:grade_id) } is redundant with it { should belong_to :grade }. belongs_to already validates presence.

Schwern
  • 153,029
  • 25
  • 195
  • 336
  • I've made as per your recomendation and worked just fine. Thank you. – Wagner Braga Dec 23 '21 at 17:34
  • 1
    @wagnerbraga You're welcome. I noticed a subtle issue with your book factory. `name { Faker::Book.title }` might create books with duplicate names and cause random test failures. Use [`unique`](https://github.com/faker-ruby/faker#ensuring-unique-values) to avoid that. `name { Faker::Book.unique.title }` – Schwern Dec 23 '21 at 22:06