I'm having troubles testing an ActiveRecord inclusion validation in Rails with Factory Girl and Rspec. The inclusion validation always fails. Here is my code:
class FruitType
has_many :fruits
end
class Fruit
belongs_to :fruit_type
validates :fruit_type_id, numericality: { only_integer: true }
validates :fruit_type_id, inclusion: { in: FruitType.pluck(:id), message: "is invalid" }
end
FactoryGirl.define do
factory :fruit_type_apple, class: FruitType do
name "Apple"
end
end
FactoryGirl.define do
factory :valid_fruit, class: Fruit do
name "Red Apple"
association :fruit_type, factory: :fruit_type_apple
end
end
Rspec test is:
it "should have valid factory" do
f = FactoryGirl.build( :valid_fruit )
puts f.fruit_type_id
puts "\n#{FruitType.all.pluck(:id)}"
expect(f).to be_valid
end
Result is:
1
[1]
F..........
Failures:
1) Fruit when validated should have valid factory
Failure/Error: expect(f).to be_valid
expected # to be valid, but got errors: Fruit type is invalid
As you can see, I've printed out the Fruit Type id list in the test, which includes only 1. And I've printed out the value of fruit_type_id for the fruit, which is 1. Yet, the inclusion validation still fails.
If I do the same thing in the rails console just by creating fruits and types manually, the validation works fine, it's just when I run the test I'm seeing this behavior. Any ideas? I must be missing something about Factory Girl here.
You shouldn't validate the fruit_type_id attribute, rather use presence validation
validates :fruit_type, presence: true
Related
I have a Rails 5 setup where RSpec fails to check validations on model subclass. If I manually build the object in console I am able to see the errors which should prevent the record to be valid.
The base model:
class Article < ApplicationRecord
belongs_to :author, class_name: User
validates :author, presence: { message: "L'utente autore dell'articolo è obbligatorio." }
validates :title, presence: { message: "Il titolo dell'articolo è obbligatorio." }
end
The model which inherits from Article:
class LongArticle < Article
mount_uploader :thumbnail, LongArticleThumbnailUploader
validates :excerpt, presence: { message: "L'estratto dell'articolo è obbligatorio." }
validates :thumbnail, presence: { message: "L'immagine di anteprima dell'articolo è obbligatoria." }
end
The factory for these models (FactoryGirl):
FactoryGirl.define do
factory :article do
association :author, factory: :author
title "Giacomo Puccini: Tosca"
factory :long_article do
type "LongArticle"
excerpt "<p>Teatro alla Scala: immenso Franco Corelli.</p>"
thumbnail { Rack::Test::UploadedFile.new(File.join(Rails.root, 'spec', 'support', 'images', 'unresized-long-article-thumbnail.jpg')) }
end
end
end
This is the RSpec which doesn't work:
require 'rails_helper'
RSpec.describe LongArticle, type: :model do
describe "is valid with mandatory fields" do
it "should be valid with if all mandatory fields are filled" do
article = FactoryGirl.create(:long_article)
expect(article).to be_valid
end
it "should have an excerpt" do
article = FactoryGirl.create(:long_article)
article.excerpt = nil
expect(article).not_to be_valid
end
it "should have the thumbnail" do
article = FactoryGirl.create(:long_article)
article.thumbnail = nil
expect(article).not_to be_valid
end
end
end
The first spec pass, the other two don't.
I tried to test everything in the console, with the same values, and it works, meaning that the record is invalid as it should be.
Is it possible that with RSpec the validations in the subclass won't work?
I'm sorry for the delay, but I think I have figured out what was breaking my tests.
The problems, actually, were two, and not one as I originally thought.
The first test: should have an excerpt
As suggested by juanitofatas, I have added a byebug line after the one where FactoryGirl builds my model. I have noticed that the model instantiated had class Article and not LongArticle.
I came up noticing that FactoryGirl instantiate a model of the base factory when it first met factory :article do. Then, it adds or overrides the attributes defined into inner factories and treating the type attribute as any one other, ignoring that it drives the STI.
The LongArticle factory should have been defined as a completely different model, at the same level as the Article one is.
The second test: should have the thumbnail
This was a bit silly... I have defined a default_url method in the CarrierWave uploader and, in fact, this is the desired behavior. Test was updated.
I have a shoulda matcher in my achievement_spec.rb and I can't get it pass:
Spec:
- require 'rails_helper'
RSpec.describe Achievement, type: :model do
describe 'validations' do
it { should validate_presence_of(:title) }
it { should validate_uniqueness_of(:title).case_insensitive.scoped_to(:user_id).with_message("you
can't have two achievements with same title")}
it { should validate_presence_of(:user) }
it { should belong_to(:user) }
end
end
Model:
- class Achievement < ActiveRecord::Base belongs_to :user
validates :title, presence: true
validates :user, presence: true
validates :title, uniqueness: {
scope: :user_id,
message: "you can't have two achievements with the same title"
}
enum privacy: [ :public_access, :private_access, :friends_access ]
def description_html
Redcarpet::Markdown.new(Redcarpet::Render::HTML).render(description)
end
end
Error:
- .F..
Failures:
1) Achievement validations should validate that :title is
case-sensitively unique within the scope of :user_id, producing a
custom validation error on failure
Failure/Error: it { should validate_uniqueness_of(:title).scoped_to(:user_id).with_message("you
can't have two achievements with same title") }
Achievement did not properly validate that :title is case-sensitively
unique within the scope of :user_id, producing a custom validation error
on failure.
After taking the given Achievement, setting its :title to ‹"an
arbitrary value"›, and saving it as the existing record, then making a
new Achievement and setting its :title to ‹"an arbitrary value"› as
well and its :user_id to a different value, ‹nil›, the matcher
expected the new Achievement to be invalid and to produce the
validation error "you can't have two achievements with same title" on
:title. The record was indeed invalid, but it produced these
validation errors instead:
* user: ["can't be blank"]
* title: ["you can't have two achievements with the same title"]
# ./spec/models/achievement_spec.rb:29:in `block (3 levels) in <top (required)>'
Finished in 0.16555 seconds (files took 3.13 seconds to load) 4
examples, 1 failure
Failed examples:
rspec ./spec/models/achievement_spec.rb:29 # Achievement validations
should validate that :title is case-sensitively unique within the
scope of :user_id, producing a custom validation error on failure
[Finished in 4.1s with exit code 1] [cmd: ['rspec', '-I
/home/mayur/Downloads/MyWork/ruby/i-rock/spec/models',
'/home/mayur/Downloads/MyWork/ruby/i-rock/spec/models/achievement_spec.rb']]
[dir: /home/mayur/Downloads/MyWork/ruby/i-rock] [path:
/usr/local/rvm/gems/ruby-2.2.4/bin:/usr/local/rvm/gems/ruby-2.2.4#global/bin:/usr/local/rvm/rubies/ruby-2.2.4/bin:/usr/lib64/qt-3.3/bin:/usr/local/bin:/usr/local/sbin:/usr/bin:/usr/sbin:/usr/local/rvm/bin:/home/mayur/.local/bin:/home/mayur/bin]
How can I get rid of above error?
I tried below solution but getting same error:
Change in model:
validates :title, uniqueness: {
case_sensitive: false,
scope: :user_id,
message: "you can't have two achievements with the same title"
}
Change in spec:
it { should validate_uniqueness_of(:title).case_insensitive.scoped_to(:user_id).with_message("you can't have two achievements with same title") }
Error Again:
.F..
Failures:
1) Achievement validations should validate that :title is case-insensitively unique within the scope of :user_id, producing a custom validation error on failure
Failure/Error: it { should validate_uniqueness_of(:title).case_insensitive.scoped_to(:user_id).with_message("you can't have two achievements with same title") }
Achievement did not properly validate that :title is case-insensitively
unique within the scope of :user_id, producing a custom validation error
on failure.
After taking the given Achievement, setting its :title to ‹"an
arbitrary value"›, and saving it as the existing record, then making a
new Achievement and setting its :title to ‹"an arbitrary value"› as
well and its :user_id to a different value, ‹nil›, the matcher
expected the new Achievement to be invalid and to produce the
validation error "you can't have two achievements with same title" on
:title. The record was indeed invalid, but it produced these
validation errors instead:
* user: ["can't be blank"]
* title: ["you can't have two achievements with the same title"]
# ./spec/models/achievement_spec.rb:29:in `block (3 levels) in <top (required)>'
Finished in 0.13566 seconds (files took 3.14 seconds to load)
4 examples, 1 failure
Failed examples:
rspec ./spec/models/achievement_spec.rb:29 # Achievement validations should validate that :title is case-insensitively unique within the scope of :user_id, producing a custom validation error on failure
The failure is occurring due to the error message defined in your spec not matching what's in your model. You're missing the word the in your spec:
Spec
... .with_message("you can't have two achievements with same title")}
Model
... message: "you can't have two achievements with the same title"
Fixing what's defined in your spec in .with_message to match what's defined in your model within the uniqueness validation message on title should resolve the failure.
You need to build a factory and ensure there is a user before running these validations:
describe Achievment do
context 'validations' do
before { FactoryGirl.build(:achievement) }
it do
should validate_uniqueness_of(:title).
scoped_to(:user_id).
case_insensitive
end
end
end
your factory then:
FactoryGirl.define do
factory :achievement
title 'some-string'
user
end
end
I'm trying to test a system for creating article translations where there is a self-join on the publications table. I've created a factory that will create multiple translations and associate them with a 'parent' article.
Using Rails 5 with factory_girl 4.7.0, rspec, and Database_cleaner
All actions work as expected, but creating a test is the problem
Here's the relevant model validations and methods:
# models/publication.rb
has_many :translations, class_name: "Publication", foreign_key: "translation_id", dependent: :nullify
belongs_to :translation, class_name: "Publication", optional: true
validates :language, uniqueness: { scope: :translation_id }, if: :is_translation?
def is_translation?
!translation.nil?
end
Factory (irrelevant code omitted):
# spec/factories/publication.rb
factory :publication, aliases: [:published_pub] do
title 'Default Title'
language 'EN'
published
after(:build) do |object|
create(:version, publication: object)
end
#-- This is where I suspect the problem stems from
trait :with_translations do
association :user, factory: :random_user
after(:build) do |object|
create_list(:translation, 3, {user: object.user, translation:object})
end
end
end
factory :translation, class: Publication do
sequence(:title) { |n| ['French Article', 'Spanish Article', 'German Article', 'Chinese Article'][n]}
sequence(:language) { |n| ['FR', 'ES', 'DE', 'CN'][n]}
user
end
And a basic test:
# spec/models/publication_spec.rb
before(:each) do
#translation_parent = create(:publication, :with_translations)
#pub_without_trans = create(:publication, :with_random_user)
end
scenario 'is_translation?' do
# No actual test code needed, this passes regardless
end
scenario 'has_translations?' do
# No actual test code needed, this (and subsequent tests) fail regardless
end
Finally, the error:
Failure/Error: create_list(:translation, 3, {user: object.user, translation:object})
ActiveRecord::RecordInvalid:
Validation failed: Language has already been taken
The first test passes (and the publication object with translations is created correctly)but any subsequent test fails. The issue is that I have a uniqueness validation scoped to translation_id and it appears that factorygirl is trying to add the generated translations to an already existing publication instead of creating an entirely new publication.
Any help is appreciated!
Solved!
The issue was that the sequence iterator in the translation factory was not resetting to 0 after each test. So after test 1, it started trying to access an array index that didn't exist. After it did that one more time, it triggered the validation and the tests failed!
The solution is not cute, but it's good enough for the time being
sequence(:language) do |iteration|
array = ['FR', 'ES', 'DE', 'CN']
# Returns a number between 0 and array.length
array[iteration%array.length]
end
sequence(:title) do |iteration|
array = ['French Article', 'Spanish Article', 'German Article', 'Chinese Article']
# Returns a number between 0 and array.length
array[iteration%array.length]
end
Does anyone know how to check for invalid enum responses in rspec? I can check to ensure that the enum isn't nil with no errors but when I check to ensure that a bad enum value doesn't work, I get an error. These are the two specs:
it 'is not valid without question type' do
expect(build(:question, question_type: nil)).to have(1).errors_on(:question_type)
end
it 'is not valid with a bad question type' do
expect(build(:question, question_type: :telepathy)).to have(1).errors_on(:question_type)
end
This is what my model looks like:
class Question < ActiveRecord::Base
enum question_type: [ :multiple_choice ]
validates :question_type, presence: true
end
This is the error:
Failure/Error: expect(build(:question, question_type: :telepathy)).to have(1).errors_on(:question_type)
ArgumentError:
'telepathy' is not a valid question_type
This is my factory:
FactoryGirl.define do
factory :question, :class => 'Question' do
question_type :multiple_choice
end
end
Thanks for the help!
Your issue is that you need expect to execute the build as a proc to evaluate the result. Do this by changing the parentheses to curly brackets, as described in https://stackoverflow.com/a/21568225/1935918
it 'is not valid with a bad question type' do
expect { build(:question, question_type: :telepathy) }
.to raise_error(ArgumentError)
.with_message(/is not a valid question_type/)
end
I have the following factory:
FactoryGirl.define do
factory :poem do
skip_create
title "Poem title"
intro_verse
trait_verse
message_verse
end
end
for the following non active record model class:
class Poem
attr_accessor :title, :intro_verse, :trait_verse, :message_verse
end
Can I create a factory for such a class?
When I run the following test:
it "has a valid factory" do
expect(build(:poem)).to be_valid
end
I get the following error:
Failure/Error: expect(build(:poem)).to be_valid
NoMethodError:
undefined method `valid?'
The error is because the class does not have an instance method valid?. (Active Record models have this defined by default)
You need to come up with some logic for determining whether a Poem instance is valid or not, and write a valid? method accordingly.
IIRC, the syntax expect(something).to be_condition simply calls the method condition? on something and fails if it returns false.
Use the ActiveModel::Validations module to adds the ability to validate class objects like in Active Record:
class Poem
include ActiveModel::Validations
validates :title, presence: true
attr_accessor :title, :intro_verse, :trait_verse, :message_verse
end
poem = Poem.new
poem.valid? #false
poem.title = "title"
poem.valid? #true