RSpec: Factory definition for associated object - ruby-on-rails

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

Related

Factory bot creates incorrect data

Why is this test passing? I don't understand what the problem is: Factory bot or Rails?
Model:
class Vote < ApplicationRecord
belongs_to :user
belongs_to :votable, polymorphic: true
validate :self_like
private
def self_like
errors.add(:user, 'self-like') if votable.author_id == user_id
end
end
Factory:
FactoryBot.define do
factory :vote do
value { 1 }
user
association :votable, factory: :question
end
end
If you output the tested object (pp vote), then all the attributes will be nil. In this case, it is possible to get the associated object (pp vote.votable)
describe 'validate :self_like' do
let!(:vote) { build :vote }
it "self-like" do
vote.valid?
expect(vote.errors[:user]).to include('self-like')
end
end
The way the factories are defined you don't specify that votable author and user match, so votable.author_id == user_id is going to be false.
The best solution I could think of is to update the votable author with an after_build hook and force it to match the user.
after(:build) do |vote|
vote.votable.update!(author: vote.user)
end

Rails FactoryGirl for model that belongs_to 2 other models

I have 3 following models like this:
# model/timeline.rb
class Timeline
belongs_to :series
belongs_to :creator
end
def series_belongs_to_creator
if creator_id
creator = Creator.find_by id: creator_id
related_series = creator.series.find_by id: series_id
errors.add(:series_id, :not_found_series) unless related_series
end
end
# model/creator.rb
class Creator
has_many :timelines
has_many :series, through: :contents
end
# model/series.rb
class Series
has_many :timelines
has_many :creators, through: :contents
end
This is not many to many relation, timelines table has two fields creator_id and series_id beside another fields. creator_id and series_id must be entered when create Timeline and i have a method series_belongs_to_creator to validates series_id must belong to creator_id to create successful.
So how should I write factory for timeline model if using FactoryGirl. Im so confused about Unit test in Rails.
If you're using Rails 5, you have to keep in mind that belongs_to is no longer optional by default: https://blog.bigbinary.com/2016/02/15/rails-5-makes-belong-to-association-required-by-default.html
So creator_id will always need to be present unless you specify the relation is optional.
For the factories, you're going to end up with something like this (FactoryGirl was recently renamed to FactoryBot):
http://www.rubydoc.info/gems/factory_bot/file/GETTING_STARTED.md#Associations
FactoryBot.define do
factory :timeline do
creator
series
end
end
FactoryBot.define do
factory :creator do
...
end
end
FactoryBot.define do
factory :series do
...
end
end

FactoryGirl Callbacks

I have the following associations and am trying to implement some factories that allow me to test fully with a has_many and has_many_through association
class Image < ActiveRecord::Base
has_many :categories
end
class Category < ActiveRecord::Base
belongs_to :image
has_many :image_categories
has_many :images, through: :image_categories
end
class ImageCategory < ActiveRecord::Base
# Holds image_id and category_id to allow multiple categories to be saved per image, as opposed to storing an array of objects in one DB column
belongs_to :image
belongs_to :category
end
So with ImageCategory I am under the impression that when i save an Image object the image_id and category_id will save in the ImageCategory Table? I have yet to see this mind in my application
So when creating a factory this is what i have so far
FactoryGirl.define do
factory :image do
title 'Test Title'
description 'Test Description'
photo File.new("#{Rails.root}/spec/fixtures/louvre_under_2mb.jpg")
factory :image_with_category, parent: :image do
categories { build_list :category, 1 }
end
factory :image_no_title do
title nil
end
factory :image_no_description do
description nil
end
factory :image_no_category, parent: :image do
categories { build_list :category, 0 }
end
end
end
I don't understand what the integer value is after the category in build_list? is it the number of instances ?
This test will pass
it 'is valid with an associated Category' do
expect(FactoryGirl.build(:image_with_category)).to be_valid
end
yet this one will fail
it 'is invalid with no category' do
#image = FactoryGirl.build(:image_no_category)
#image.save
expect(#image.errors[:category]).to eq(["Don't forget to add a Category"])
end
expected: ["Don't forget to add a Category"]
got: []
What have i done wrong here?
Thanks
You are correct the build_list is the FactoryGirl helper for building instances. And in your second test you don't have any errors because you don't have any validation at all.

validation error through FactoryGirl

I have problems on validating my data through FactoryGirl.
For team.rb, the custom validations are has_only_one_leader and belongs_to_same_department
class Team < ActiveRecord::Base
attr_accessible :name, :department, :active
has_many :memberships
has_many :users, through: :memberships
accepts_nested_attributes_for :memberships, :users
validates :department, inclusion: [nil, "Architectural", "Interior Design"]
validates_uniqueness_of :name, scope: :department, conditions: -> { where(active: true) }
validate :has_only_one_leader
validate :belongs_to_same_department
def has_only_one_leader
unless self.users.where!(designation: "Team Leader").size == 1
errors.add(:team, "needs to have exactly one leader")
end
end
def belongs_to_same_department
unless self.users.where!.not(department: self.department).size == 0
errors.add(:users, "should belong to the same department")
end
end
I'll also include membership.rb and user.rb (associations only) just for reference.
class Membership < ActiveRecord::Base
belongs_to :team
belongs_to :user
end
class User < ActiveRecord::Base
has_many :memberships
has_many :teams, through: :memberships
end
Here's my factory for team.rb
FactoryGirl.define do
factory :team do
sequence(:name) {|n| "Team #{n}" }
department "Architectural"
before(:create) do |team|
team.users << FactoryGirl.create(:user, designation: "Team Leader",
department: "Architectural")
team.users << FactoryGirl.create_list(:user, 5,
designation: "CAD Operator", department: "Architectural")
end
end
end
It seems that after I do FactoryGirl.create(:team) in the rails console, it seems that I got the error messages from my validations.
Two things I've noticed when I manually build a team, specifically adding members to the team with 1 leader and 5 members belonging to the same department:
team.users.where!(designation: "Team Leader").size returns 6 although there's only one leader, same goes for changing the designation to CAD Operator, returns 6 although there are only five non leaders in the team.
same goes for checking if the members are in the same department, team.users.where!.not(department: team.department).size returns 6, although all of them belong to the same department.
Are there any modifications to make in my model or in my factory?
Here are the things that I've discovered before getting the answer.
team.users.where(designation: "Team Leader") returns a User::ActiveRelation_AssociationRelation object
team.users returns a User::ActiveRecord_Associations_CollectionProxy object
CollectionProxy has no method where since the this method (in my opinion) is a query to the database (with exception if the team is already saved in the database, you can use where, but in this case, it's only for building)
Therefore, I used the select method accompanied with the count, to return the correct values like so. I'll use my example from the question to illustrate the answer.
team.users.select(:designation) {|user| user.designation == "Team Leader"}.count returns 1
team.users.select(:department) {|user| user.department != team.department}.count returns 0

Factory Girl and nested attributes validation error

I have a model Company which accepts nested attributes for Recruiters model. I need to have validation in my Company model that at least one recruiter was created during Company creation.
class Company < ActiveRecord::Base
has_many :recruiters, dependent: :destroy, inverse_of: :company
accepts_nested_attributes_for :recruiters, reject_if: ->(attributes) { attributes[:name].blank? || attributes[:email].blank? }, allow_destroy: true
validate { check_recruiters_number } # this validates recruiters number
private
def recruiters_count_valid?
recruiters.reject(&:marked_for_destruction?).count >= RECRUITERS_COUNT_MIN
end
def check_recruiters_number
unless recruiters_count_valid?
errors.add(:base, :recruiters_too_short, count: RECRUITERS_COUNT_MIN)
end
end
end
Validation works as expected but after adding this validation I have a problem with FactoryGirl. My factory for company looks like this:
FactoryGirl.define do
factory :company do
association :industry
association :user
available_disclosures 15
city 'Berlin'
country 'Germany'
ignore do
recruiters_count 2
end
after(:build) do |company, evaluator|
FactoryGirl.create_list(:recruiter, evaluator.recruiters_count, company: company)
end
before(:create) do |company, evaluator|
FactoryGirl.create_list(:recruiter, evaluator.recruiters_count, company: company)
end
end
end
In tests, when I do
company = create(:company)
I get validation error:
ActiveRecord::RecordInvalid:
Validation failed: Company has to have at least one recruiter
When I first build company and then save it, the test passes:
company = build(:company)
company = save
Of course, I don't want to change all my tests this way to make them work. How can I set up my factory to create associated model during creation Company model?
Your validate { check_recruiters_number } is unreasonable. Remove it.
Why? You need to have a valid company id to save recruiters, but your validator prevent company to be valid because it has no recruiters. This is in contradiction.
This is an old question but I have similar problem and I solved it with the following code (rewritten to match your case):
FactoryGirl.define do
factory :company do
association :industry
association :user
available_disclosures 15
city 'Berlin'
country 'Germany'
ignore do
recruiters_count 2
end
after(:build) do |company, evaluator|
company.recruiters = FactoryGirl.build_list(:recruiter, evaluator.recruiters_count, company: company)
end
end
end

Resources