has_many relationship using FactoryGirl - ruby-on-rails

I'm trying to create instance with has_many relationship using FactoryGirl factories and fail.
I have two classes: Computers and NetworkCards. Each Computer can have many NetworkCards.
Defined two ActiveRecords:
1.
class Computer < ActiveRecord::Base
has_many :network_cards
end
2.
class Article < ActiveRecord::Base
belongs_to: computer
end
Defined the following factories:
1.
factory :computer do
sequence(:name) { |n| "PC_#{n}" }
transient do
network_cards_count 1
end
after(:create) do |computer|
create_list(:network_card, evaluator.network_cards_count, computer: computer)
done
end
2.
factory :network_card do
sequence(:name) { |n| "NC_#{n}" }
sequence(:type) { |n| "TYPE_#{n}" }
end
Now, when creating a computer in a RSpec test I get a weird behavior I can't explain.
#computer_1 = FactoryGirl.create(:computer)
#computer_1.network_cards.size #Expect 1. Got 0. Why?
#computer_1.reload
#computer_1.network_cards.size # Got 1
Any ideas what am I missing?

Try this:
after(:build) do |computer, evaluator|
computer.network_cards << build_list(:network_card, evaluator.network_cards_count, computer: computer)
end

factory :computer do
sequence(:name) { |n| "PC_#{n}" }
transient do
network_cards_count 1
end
after(:create) do |computer, evaluator|
create_list(:network_card, evaluator.network_cards_count, computer: computer)
computer.reload #<----- solves the issue
end
end

Related

In Rails 6 and FactoryGirl 4.9, how do I tie the original object back its has_many relation that's being created?

I’m using Rails 6 with FactoryGirl 4.9.0. I have these models, one has a has_many with the other …
class Contract < ApplicationRecord
…
belongs_to :client
class Client < ApplicationRecord
…
has_many :contracts
In my factory, I have
FactoryGirl.define do
factory :client do
name "Test Client”
end
factory :dp_client, class: Client do
Contracts { [create(:contract)] }
end
end
But when I go to use the factory,
create(:dp_client)
I get the error …
Validation failed: Client must exist
When creating the "has_many" relationship in jn my factory, I do I tie it back to the object that's creating it?
FactoryBot.define do
factory :client do
name { "Test client" }
end
factory :contract do
client
end
factory :dp_client, class: Client do
name { "client name" }
after(:create) do |client|
create(:contract, client: client)
end
end
end
It's done like this:
FactoryBot.define do
factory :client do
name { "Test client" }
end
factory :contract do
association :client
end
factory :dp_client, class: Client do
name { "Test DP Client"}
contracts { [ FactoryBot.create(:contract) ]}
end
end
There's multiple ways to deal with this.
Option 1:
Use the instance in your block. FactoryBot has self set to the factory and also passes it in as an argument. So both of these should work:
contracts { |factory| [build(:contract, client: factory.instance)] }
# or
contracts { [build(:contract, client: instance)] } # because self is the factory
Note: I use build here instead of create because of how Rails autosaves associations. Both should work fine in most use cases.
Option 2: Use after_create
This is the solution that was proposed by Saiqul Haq. Main downside: It exclusively works for create (not for build). Also noteworthy: It might not reflect the reality of your app (e.g. if your records are always created together).
Option 3: Use before_create
Similar to the previous solution, but potentially more realistic in terms of workflow:
before(:create) do |client|
client.build_contract(attributes_for(:contract))
end

Factory fails after changing name

I have a factory that work's fine when its symbol is :notification_event but when I change the name to player_notification_event it fails with the error
uninitialized constant PlayerNotificationEvent.
Also, my other factory :property_notification_event is failing as well, with the error
uninitialized constant PropertyNotificationEvent.
failing factories
factory :player_notification_event do
notification_eventable_type 'Player'
association :notification_eventable, factory: :player
unread_count 1
last_notif_unread_count 0
last_email_message_count 0
last_email_time 5.hours.ago
last_notif_time 3.hours.ago
end
factory :property_notification_event do
notification_eventable_type 'Property'
association :notification_eventable, factory: :property
unread_count 1
last_notif_unread_count 0
last_email_message_count 0
last_email_time 5.hours.ago
last_notif_time 3.hours.ago
end
failing specs
let(:player_notification_event) { create :player_notification_event }
let(:property_notification_event) { create :property_notification_event }
it 'sends email to player' do
player = player_notification_event.notification_eventable
allow(UnreadMessagesMailer).to receive_message_chain(:player_email, :deliver_now!)
described_class.perform
expect(UnreadMessagesMailer).to have_received(:player_email)
end
it 'sends email to property' do
property = property_notification_event.notification_eventable
allow(UnreadMessagesMailer).to receive_message_chain(:property_email, :deliver_now!)
described_class.perform
expect(UnreadMessagesMailer).to have_received(:property_email)
end
passing spec
let(:player_notification_event) { create :notification_event }
it 'sends email to player' do
player = player_notification_event.notification_eventable
allow(UnreadMessagesMailer).to receive_message_chain(:player_email, :deliver_now!)
described_class.perform
expect(UnreadMessagesMailer).to have_received(:player_email)
end
passing factory
factory :notification_event do
notification_eventable_type 'Player'
association :notification_eventable, factory: :player
unread_count 1
last_notif_unread_count 0
last_email_message_count 0
last_email_time 5.hours.ago
last_notif_time 3.hours.ago
end
You can use inheritance here instead of duplicating the entire factory.
[...] it's good practice to define a basic factory for each class with only
the attributes required to create it. Then, create more specific
factories that inherit from this basic parent. Factory definitions are
still code, so keep them DRY.
https://www.rubydoc.info/gems/factory_bot/file/GETTING_STARTED.md
factory :notification_event do
unread_count 1
last_notif_unread_count 0
last_email_message_count 0
last_email_time 5.hours.ago
last_notif_time 3.hours.ago
factory :player_notification_event do
notification_eventable_type 'Player'
association :notification_eventable, factory: :player
end
factory :property_notification_event do
notification_eventable_type 'Property'
association :notification_eventable, factory: :property
end
end
Since the model class is derived from the parent factory :notification_event you don't need to manually specify it.
The default of factory_bot is to look for a class with the same name as first argument in factory, if you don't pass a class explicitly (check official guide). Try this:
factory :player_notification_event, class: NotificationEvent do ...

FactoryGirl: set has_many association on creation

This issue is happening in an upgrade that I'm working from Rails 3.2 to Rails 4.2. It might be related to the many things that Rails 4 broke.
I have the following factory:
factory :account do
sequence(:email) {|n| "email#{n}#example.org" }
sequence(:name) {|n| "Name #{n}" }
end
This model has an has_many association called ips. At a specific test, I need to set the account with an ip. In Rails 3.2 I was able to do this:
FactoryGirl.create(:account, :ips => [FactoryGirl.create(:ip)])
But in Rails 4 I get an exception:
ActiveRecord::RecordInvalid: Validation failed: Ips is invalid
I was able to verify that I'm not longer able to override an has_many association. Example:
account = FactoryGirl.build(:account)
account.ips = [FactoryGirl.create(:ip)]
account.save!
It will also throw an exception.
As a side note, using << works fine but that's not what I want since I want to delete any IPs that are assigned to the account and only set the new one.
What is the proper way of doing this in Rails 4?
For this I would recommend using a trait with an after create. So it would look something like this:
FactoryGirl.define do
factory :account do
sequence(:email) {|n| "email#{n}#example.org" }
sequence(:name) {|n| "Name #{n}" }
end
trait :with_ip do
transient do
ip_count 5
end
after(:create) do |account, evaluator|
create_list(:ip, evaluator.ip_count, account: account)
end
end
end
See this for further reference: https://github.com/thoughtbot/factory_girl/blob/master/GETTING_STARTED.md

How should I set up complex Factories in Rails?

In my rails application I have three objects I would like to model for a particular Rspec test. These are Organizations, Users, and Sales Opportunities.
class Organization
has_many :users
end
class User
has_many :sales_opportunities
end
An Organization has to have a unique name, and for the authorisation/authentication aspects of my app to work, both users need to belong to the same Organization. For the purposes of this test I want one User to be admin and one to be non-admin.
This works successfully in other parts of my application (e.g. sign up - the first user is automatically the admin, subsequent users are not, and for viewing links etc on pages). However what I now need to do is create two users (both belong_to one organization), make one of them the admin, and then when I delete the sales_opportunity non-admin user any sales_opportunities that belonged to this non-admin user should transfer ownership to the admin (for re-allocation).
FWIW I am using associations for some of the other tests - but in this case I can't create two users associated with one organization, so I've used:
before(:create) do |organization|
organization.users << FactoryGirl.build(:user ...admin params)
organization.users << FactoryGirl.build(:user ...non_admin params)
end
in the factory for the organization to build 2 users who both belong to one organization and have different characteristics. This works well.
If I try to add another before(:create) to the factory to then build a sales_opportunity it fails (because the users aren't created yet I believe). After(:create) statements also fall flat. I've also tried defining the non_admin user in the tests themselves (e.g. by using a let(:non_admin) = Organization.users.last and then non_admin.sales_opportunities.create statement), but this fails to product any sales opportunities (not sure what it does).
Are there any good resources on how to build tests like this? I can write the code to solve the problems in no time - I seem to waste a huge amount of time writing the tests first. I'm sure these are not particularly DRY either looking through my factories.
Any help would be appreciated.
in rspec/factories/sales_opportunities.rb:
FactoryGirl.define do
sequence(:name) { |n| "Sales Oppotunity - #{n}" }
factory :sales_opportunity do
user
name { generate(:name) }
# add more attributes here
end
end
in rspec/factories/users.rb:
FactoryGirl.define do
factory :user do
organization
sequence(:name) { |n| "Test user #{n}" }
sequence(:email) { |n| "test.user#{n}#example.com" }
password "password"
admin false
# add more attributes here
end
factory :admin_user, parent: :user do
admin true
end
factory :user_with_sales_opportunities, parent: :user, traits: [:with_sales_opportunity]
factory :admin_user_with_sales_opportunities, parent: :admin_user, traits: [:with_sales_opportunity]
trait :with_sales_opportunity do
ignore do # instead of ignore use transient if you're using factory girl 4.3 or above
sales_opportunities_count 5
end
after(:create) do |user, evaluator|
sales_opportunities { FactoryGirl.create_list(:sales_opportunity, evaluator.sales_opportunities_count, user: user) }
end
end
end
in rspec/factories/organizations.rb:
FactoryGirl.define do
sequence(:name) { |n| "Organization - #{n}" }
factory :organization do
name { generate(:name) }
ignore do
users_count 5
admin_users_count 2
sales_opportunities_count 5
end
after(:create) do |organization, evaluator|
users { FactoryGirl.create_list(:user_with_sales_opportunities, evaluator.users_count, organization: organization, sales_opportunities_count: sales_opportunities_count) + FactoryGirl.create_list(:admin_user_with_sales_opportunities, evaluator.users_count, organization: organization, sales_opportunities_count: sales_opportunities_count) }
end
# add more attributes here
end
end
Now you can:
let(:organization) { FactoryGirl.create(:organization, users_count: 4, admin_users_count: 3, sales_opportunities_count: 4) }
Im not sure exactly what you're trying to do, as you seem to be deleting the sales opportunity that you want to transfer?
If you think its an rspec you could instead try using after(:build)
Something like:
factory :user, class: User do
sequence(:email) { |n| "user#{n}#example.org" }
password "foobar"
# Standard user stuff etc
trait :admin do
after(:build) do |user|
user.set_admin
end
end
end
factory :organisation, class: Organisation do
name "Super Duper"
after(:build) do |organization|
organization.users << FactoryGirl.build(:user)
organization.users << FactoryGirl.build(:user, :admin)
end
end
I feel like you may have a modeling issue though. Shouldn't your sales opportunities be standalone?
ie:
class SalesOpportunity
field :name, type: String
field :phone, type: Integer
belongs_to :user
end
class User
field :name, type: String
field :phone, type: Integer
has_many :salesopportunities
end
This way you can delete a user object and persist the sale opportunity?

Test validation that makes sure record is associated to records only owned by parent with spec

I have model Category which has and belongs to many Packages. When a user creates a Posting in a Category, it must belong to a Package that is in its category.
Here's the code for the custom validator:
class PackageValidator < ActiveModel::Validator
def validate(record)
record.errors[:package_id] <<
I18n.t('errors.messages.package.not_in_category') unless
record.category.packages.include? record.package
end
end
This works great and all, but it breaks all my rspec tests (with factory_girl) because factory_girl creates a new Package for each Category and Posting I build/create with it.
Here's my factories:
factory :category do
sequence(:name) {|n| "Parts#{n}"}
sequence(:slug) {|n| "parts#{n}"}
packages {[FactoryGirl.create(:package)]}
end
factory :posting do
title 'Foo'
body 'Bar'
category
package
end
factory :package do
sequence(:name) {|n| "Basic#{n}"
price 0
photos 5
end
How can I make it all play nicely together?
You can set the package to category.packages.first in an after(:build) hook.

Resources