Rspec testing association results in failure - ruby-on-rails

I'm trying to create "components" for my project and each component belongs to one of four types (Section, Composition, LearningIntentions, or EssentialQuestion). A section is a component and can contain multiple components, but a section cannot contain a section.
But when I run my spec, I get the error Expected Component to have a has_many association called components (no association called components). But it works when I move the spec from section_spec.rb to component_spec.rb. What is happening here?
component.rb (model):
class Component < ApplicationRecord
# == Constants =============================
TYPES = %w( Section Composition LearningIntentions EssentialQuestion )
# == Associations ==========================
belongs_to :project
belongs_to :section,
class_name: 'Component',
foreign_key: :section_id,
optional: true
# # == Validations ===========================
validates :type,
presence: true,
inclusion: { in: TYPES }
end
section.rb (model):
class Section < Component
# == Associations ==========================
has_many :components,
class_name: 'Component',
foreign_key: :section_id
# == Validations ===========================
validates :section_id, presence: false
end
section_spec.rb:
RSpec.describe Section, type: :model do
subject! { build(:component, :section) }
# == Associations ==========================
it { is_expected.to have_many(:components) }
end

Reason for failed test
But it works when I move the spec from section_spec.rb to component_spec.rb
The location of your rspec is not the cause of failure here, as it looks to me. What you are doing wrong is, assigning a Component instance in your section_spec.rb as the subject and so, it expects the association :components to be present on that Component instance.
Fix for failed test
Change the code to below and see if it works:
RSpec.describe Section, type: :model do
subject { build(:section) }
it { is_expected.to have_many(:components) }
end
Using traits in factory
If you want to build a Section instance with components assigned to it, then define a trait and use it as below:
# Factory
factory :section do
trait :with_components do
after(:build) do |s|
# Modify this to create components of different types if you want to
s.components = [create(:component)]
end
end
end
# Rspec
subject { build(:section, :with_components) }
Couple of more things
This association in your Component model looks fishy:
belongs_to :section,
class_name: 'Component',
foreign_key: :section_id,
optional: true
Are you sure you want to link a Component instance to another Component instance?
And while doing that, you want to use foreign key :section_id instead of conventional component_id?
You already have a model Section in your application. It might cause confusions. See below:
component = Component.first
component.section
=> #<Component id: 10001> # I would expect it to be a `Section` instance looking at the association name

Related

Rails - Validate presence of one of two belongs_to columns

I have a model called Log that has relations to two models ButtonPress and LinkClick. I need to validate the presence of either (for analytics reasons, I'm required to do this without using polymorphic relations)
My Spec file is as follows:
RSpec.describe RedirectLog, type: :model do
it { is_expected.to belong_to(:button_press).optional }
it { is_expected.to belong_to(:link_click).optional }
it "has either a button_press or a link_click" do
log = build(:log, button_press: nil, link_click: nil)
expect(log).not_to be_valid
end
end
To achieve this I created the model as follows:
class Log < ApplicationRecord
belongs_to :button_press, optional: true
belongs_to :link_click, optional: true
validates :button_press, presence: true, unless: :link_click
end
When I run this, I get the following error:
Failure/Error: it { is_expected.to belong_to(:button_press).optional }
Expected Log to have a belongs_to association called button_press (and for the record not to fail validation if :button_press is unset; i.e., either the association should have been defined with `optional: true`, or there should not be a presence validation on :button_press)
So in short, I cant have the belongs_to association if I want to validate its presence, even conditionally. My question is, what is the Rails way to have a validation on one of two belongs_to columns without using polymorphism?
One way to do it is with a custom validator. I'd make it completely formal with its own file and such, but for a proof-of-concept you could do this:
class Log < ApplicationRecord
###################################################################
#
# Associations
#
###################################################################
belongs_to :button_press, optional: true
belongs_to :link_click, optional: true
###################################################################
#
# Validations
#
###################################################################
validate :button_press_or_link_click_presence
###################################################################
#
# In-Model Custom Validators (Private)
#
###################################################################
def button_press_or_link_click_presence
self.errors.add(:base, 'Either a Button Press or a Link Click is required, but both are missing.') if button_press.blank? && link_click.blank?
end
private :button_press_or_link_click_presence
end

RSpec: Factory definition for associated object

I have the following three models: Product, Warehouse and Inventory
# app/models/product.rb
class Product < ApplicationRecord
has_many :inventories
has_many :warehouses, through: :inventories
end
# app/models/warehouse.rb
class Warehouse < ApplicationRecord
has_many :inventories
has_many :products, through: :inventories
end
# app/models/inventory.rb
class Inventory < ApplicationRecord
belongs_to :product
belongs_to :warehouse
end
I have this factory for Inventory:
FactoryBot.define do
factory :inventory do
product { nil }
warehouse { nil }
item_count { 1 }
low_item_threshold { 1 }
end
end
How can I use this factory for Inventory or what changes are needed in my other factories so that I can have a spec something like this?
RSpec.describe Inventory, type: :model do
it "has a valid factory" do
expect(FactoryBot.build(:inventory)).to be_valid
end
end
What you need is to change the :inventory factory definition, like this
FactoryBot.define do
factory :inventory do
product
warehouse
item_count { 1 }
low_item_threshold { 1 }
end
end
This will "tell" factory bot to instantiate the associated objects (https://github.com/thoughtbot/factory_bot/blob/master/GETTING_STARTED.md#associations)
But for this to work, you need to define warehouse and product factories.
You can use either the create or build methods passing the name if the factory as a symbol:
it "has a valid factory" do
expect(create(:inventory)).to be_valid
end
# OR
it "has a valid factory" do
expect(build(:inventory)).to be_valid
end
create will save the model while build will simply instantiate it. If you are having trouble getting your factories loaded, ensure they are in the right place.
you might change definition of the class Inventory to:
# app/models/inventory.rb
class Inventory < ApplicationRecord
belongs_to :product, optional: true
belongs_to :warehouse, optional: true
end
and you will get successful validation
inventory = FactoryBot.build(:inventory)
inventory.valid? #true
###############################################
Explanation:
to build valid Inventory object with current definition(like in question description) of the model its necessary to initialize associated objects also. So every time validation checks if warehouse and product attributes present.
But its possible to avoid such behaviour with associations attribute optional: true.
# app/models/inventory.rb
class Inventory < ApplicationRecord
belongs_to :product
belongs_to :warehouse
end
FactoryBot.define do
factory :inventory do
product { nil }
warehouse { nil }
item_count { 1 }
low_item_threshold { 1 }
end
end
inventory = FactoryBot.build(:inventory)
inventory.valid? #false
inventory.errors.full_messages # ["Product must exist", "Warehouse must exist"]
:required
When set to true, the association will also have its
presence validated. This will validate the association itself, not the
id. You can use :inverse_of to avoid an extra query during validation.
NOTE: required is set to true by default and is deprecated. If you
don’t want to have association presence validated, use optional: true.
https://apidock.com/rails/ActiveRecord/Associations/ClassMethods/belongs_to

Should accepts_nested_attributes_for also be setting up the association correctly?

Given some kind of Thing:
class Thing < ApplicationRecord
include CustomFieldable
#...
end
Which can have custom field values attached to it:
module CustomFieldable
extend ActiveSupport::Concern
included do
has_many :custom_field_values, as: :custom_fieldable, dependent: :destroy
validates_associated :custom_field_values
accepts_nested_attributes_for :custom_field_values
end
end
And where custom field values are basically just a string value (at least for now) with a reference to their owner:
class CustomFieldValue < ApplicationRecord
belongs_to :custom_fieldable, polymorphic: true, dependent: :destroy
belongs_to :custom_field, dependent: :destroy
validates_presence_of :custom_fieldable
validates_presence_of :custom_field
validates_presence_of :string_value
end
And to the custom field, which is just a wrapper around a name:
class CustomField < ApplicationRecord
validates_presence_of :name
validates_uniqueness_of :name
end
When I initialise the Thing with a hash:
"thing"=>{
//...other stuff...
"custom_field_values_attributes"=>{
"0"=>{
"custom_field_id"=>"1",
"string_value"=>"value 1"
}
}
}
I would expect ActiveRecord to set up the association from the CustomFieldValue back to the Thing. But it looks like it is not, because I get a validation error:
There were problems with the following fields:
Custom field values custom fieldable can't be blank
Custom field values is invalid
So it's like when I use accepts_nested_attributes_for, the parent association is not set up. Is that expected behaviour?
Update #1:
Controller logic for permitting the fields looks like this:
class ThingController < ApplicationController
def thing_params(action)
common_params = [ omitting common stuff... ]
params.fetch(:licence).permit(*common_params,
custom_field_values_attributes: [
:custom_field_id, :string_value ])
end
end
Update #2:
If I write two tests for the model, I can see the same thing happening.
Fails:
test "adding a custom field value on construction via nested attributes" do
thing = Thing.new custom_field_values_attributes: [
{ custom_field_id: custom_fields(:environment).id,
string_value: 'Testing' }
]
assert_attribute_not_invalid thing, :custom_field_values
assert_equal 'Testing', thing.custom_field_values[0].string_value
end
Passes:
test "adding a custom field value via nested attributes" do
thing = things(:one)
thing.update_attributes custom_field_values_attributes: [
{ custom_field_id: custom_fields(:environment).id,
string_value: 'Testing' }
]
assert_valid thing
assert_equal 'Testing', thing.custom_field_values[0].string_value
end
So it's like, if the record isn't saved yet, Rails doesn't set up the nested models correctly, but if it's already saved, they get set up correctly.
I tried something on a whim. Changed this:
has_many :custom_field_values, as: :custom_fieldable, dependent: :destroy
To this:
has_many :custom_field_values, as: :custom_fieldable, inverse_of: :custom_fieldable, dependent: :destroy
So it seems that Rails cannot guess the inverse relationship for polymorphic associations - even though I was already forced to tell it this using :as. Specifying it twice works fine.
With the code given, I would say every thing works fine. You create a Thing object and add multiple CustomFieldValue objects. The new CustomFieldValue objects have set custom_field_id to 1. This does not mean that Rails loads the custom_field object. This would only happen when you save the object and reload it. So validates_presence_of :custom_field is right to complain. This attribute is still nil. I think the same will happen for custom_fieldable

ActiveRecord models interdependencies and correlations

I have a bunch of AR models that depend one to each other and I'm not able to figure out how to make everything work together...
First of all a Pet and a Dog model:
class Pet < ActiveRecord::Base
belongs_to :animal, polymorphic: true, dependent: :destroy
belongs_to :facility
has_many :pet_pictures, dependent: :destroy
validates :name, :birth, :facility, :pet_pictures, presence: true
end
class Dog < ActiveRecord::Base
has_one :pet, as: :animal
has_many :mixtures, as: :blended, dependent: :destroy
validates :mixtures, presence: true
end
Then both PetPictures and Mixtures:
class PetPicture < ActiveRecord::Base
belongs_to :pet
validates :photo_file, presence: true
end
class Mixture < ActiveRecord::Base
belongs_to :blended, polymorphic: true
validates :breed, presence: true
validates :breed, uniqueness: { scope: [:blended_id, :blended_type] }
end
The problem is that this is too complex and I'm having huge problem coordinating all the dependencies, I also had to remove some validations (a pic MUST have a pet related to it, but it's so difficult that I ended up removing it). It appears to me that there's no correct order of creation to end up with a valid object.
For example, take this spec:
RSpec.describe PetPicture, type: :model do
let(:dog) do
FactoryGirl.build(:marley,
mixtures: [mixture_yorkshire, mixture_dachsund],
pet: FactoryGirl.build(
:dog,
pet_pictures: [FactoryGirl.build(:marleys_pic),
FactoryGirl.build(:second_marleys_pic)],
facility: lida))
end
context 'when creating' do
it 'should be valid' do
# dog.save && dog.pet.save
dog.pet.pictures.each do |picture|
expect(picture).to be_valid
end
end
end
end
This spec passes only after saving by hand (if I FactoryGirl.create nothing is created due to validation errors related to the order of creation) BUT, and I seriously see no reason for this behavior, picture.pet_id is NULL.
Can you help me debug this? Any suggestion or link on how to improve/refactor/cleanup this mess is very welcome - just keep in mind that I adopted this for a reason, so a Pet has many pictures and can be a Dog, a Cat or whatever, which have all a different set of attributes very specific to their class. Also, a Dog can be 50% dachsund and 50% yorkshire, to explain the mixtures relation.
Thanks in advance.
I think if you are trying to check for every validations, you should do it one at a time, don't try create a bunch of them. For example:
RSpec.describe PetPicture, type: :model do
let(:pet) { FactoryGirl.create(:pet) }
let(:picture) do
FactoryGirl.build(:marleys_pic, pet: pet)
end
context 'when creating' do
it 'should be valid' do
expect(picture).to be_valid
end
end
end
And if you want something really clean and clear, try shoulda-matchers. Every associations and validations could be tested by only 1 line of code.

ActiveRecord association not being set in the database?

I’m sure this is a “duh" newbie-type question, but I’ve been at it for days and cannot figure out why my code isn’t setting a relationship in the database correctly. I have a simple belongs_to relationship between two models.
class Pod < ActiveRecord::Base
belongs_to :instigator, :class_name => “User"
attr_accessor :instigator, :instigator_id, :title
validates_presence_of :instigator, :title
validates_associated :instigator
end
and
class User < ActiveRecord::Base
has_many :instigated_pods, inverse_of: :instigator, :class_name => "Pod", as: "instigator"
end
Then I want to test them with rspec and Factory Girl using (what I think) are, again, pretty simple factories
FactoryGirl.define do
factory :pod do
title "Test pod"
instigator
end
factory :user, aliases: [:instigator] do
username
end
end
With this setup, most tests pass, but my PodsController update test kept failing, and I finally found a test that shows why.
require 'rails_helper'
RSpec.describe Pod, type: :model do
it "saves the relationship to the database" do
pod = FactoryGirl.create(:pod)
expect(pod.save).to be_truthy
expect(pod.reload.instigator).to_not be_nil # passes - cached?
pod_from_database = Pod.find(pod.id)
expect(pod_from_database.instigator).to_not be_nil # <- fails
end
end
It seems that something is preventing the pod.instigator_id from being set in the database, so the relationship isn’t persisting. And I have no clue why!!!
I tried setting validates_presence_of :instigator_id, but that makes most of the standard rspec tests fail, and I saw this from the Rails Guides:
If you want to be sure that an association is present, you'll need to test
whether the associated object itself is present, and not the foreign key used
to map the association.
class LineItem < ActiveRecord::Base
belongs_to :order
validates :order, presence: true
end
In order to validate associated records whose presence is required, you must
specify the :inverse_of option for the association:
class Order < ActiveRecord::Base
has_many :line_items, inverse_of: :order
end
Any help straightening this out would be appreciated!
While I’m not altogether clear why, it appears that removing the line
attr_accessor :instigator, :instigator_id, :title
solves the problem. It appears to be blocking writing these attributes. Any indicator why would be appreciated!
attr_accessor is a ruby method that makes a getter and a setter(make them able to be read and to be written), this is not Database fields which called attr_accessible in Rails 3.
what you did is declare an ruby methods this is why it doesn't worked.
read more here:
http://rubyinrails.com/2014/03/17/what-is-attr_accessor-in-rails/

Resources