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?
Related
I’m using Rails 6.2. I have a user factory (FactoryBot 6.2) that I set up like so
FactoryBot.define do
factory :user do
…
after(:build) do |user, vars|
…
if vars.addresses.nil?
user.addresses = build_list(:address, 1, user: user)
end
end
My user model has
has_many :addresses
And my address model has
belongs_to :user
The problem with all of the above is that these calls behave the same way
create(:user, addresses: [])
And
create(:user)
In the first case, I want an empty array to be assigned to the user object. In the second case, I would like the addresses auto-created using my address factory. How do I distinguish between passing in an empty array and not passing in anything at all?
I tend to use transients for this sort of thing to make it clear what I'm trying to do.
So something like:
FactoryBot.define do
factory :user do
transient do
with_addresses { true }
end
...
after(:build) do |user, vars|
if vars.with_addresses == true
user.addresses = build_list(:address, 1, user: user)
elsif vars.with_addresses
user.addresses = vars.with_addresses
end
end
end
end
and then:
create(:user) # Builds one address for the user
create(:user, with_addresses: []) # Starts with no addresses
create(:user, with_addresses: [a1, a2]) # Starts with two addresses
And you could make it a little smarter and more flexible so that you could say things like:
create(:user, with_addresses: 0) # To create with none
create(:user, with_addresses: 6) # To get 6 from your address factory
You'd just need a couple small tweaks to the after(:build) block.
What is the correct way to assign associations that already exist?
I am trying to assign a has_one relationship between a user and a city, where the same city can be used by many users or other entities (e.g. an event).
Code
FactoryGirl.define do
factory :user do
name 'john'
trait :in_boston do
association :city, factory: :boston
end
end
end
Error
PG::UniqueViolation: ERROR: duplicate key value violates unique constraint "city_pkey" because it's trying to create Boston twice in the database.
What I would like to do is simply reference the existing factory, not create a new one.
My current working (but less than ideal) solution
FactoryGirl.define do
factory :user do
name 'john'
trait :in_boston do
after(:create) do |user|
user.city = City.find_by_name('Boston') || create(:boston)
end
end
end
end
Any guidance would be appreciated, thanks.
So, I'm going to assume that your model code is golden, and show you how I'd setup the test. I'm not sure why you need the factory to have decision making powers based on if the city exists or not. Just instantiate the city in its own factory and call the association in your test setup.
Factories
# factories/cities.rb
FactoryGirl.define do
factory :city do
name 'Boston'
end
end
# factories/users.rb
FactoryGirl.define do
factory :user do
name 'john'
city
end
end
Test
describe 'blah' do
let( :city ){ create :city }
let( :user ){ create :user, city: city }
it 'user should have a city' do
expect( user.city.name ).to eq 'Boston'
end
end
I had the same issue when testing a model that belonged to another model, when a callback was creating that association.
To explain simply, let's say I have a Book model, and a Page model, with Page belongs_to Book, and a callback to create Page when a book is created.
In my factory for Page, I try to associate to Book, but by doing so I create book once, and the creation of the page itself create the same book again. By UniqueIndex condition, PostgreSQL fails.
The simplest solution in that case is to not to create Page when testing the Page model, but instead to simply create(:book) and then use book.page.
With FactoryGirl I know I can make singular attributes like this:
FactoryGirl.define do
factory :company do
trading_name { FFaker::Company.name }
legal_type { legal_types.keys.sample }
updated_by { FactoryGirl.create(:user) }
...
Now I'm trying to add a user, but not sure how to do it.
I tried:
users { FactoryGirl.create(:user) }
But got an error on an each loop because users wasn't the collection of users, it was changed to be that single user.
So how do you create has_many objects in a factory? How do you make sure they are linked to each other? Some people do an after(:create) but I'd like to make the users before saving the Company record.
Association section of Factory Girl documentation says to use after(:create). In your case code will be:
FactoryGirl.define do
factory :company do
...
after(:create) do |company|
create_list(:user, 3, company: company)
end
Update: In a case you have after_save validation in User model you may use after(:build) hook instead of after(:create):
after(:build) do |company|
company.users << build(:user)
end
I'm trying to do something I think should be fairly simple with Factory Girl and can't quite get it. A user has many stories and I'm testing viewing a user's profile page, which lists their created stories.
I looked up creating associated factories and the docs say I can do something like this:
FactoryGirl.define do
factory :story do
title "My Story"
segments_limit 5
beginning "Once upon a time"
completion_status false
user
end
factory :user do
sequence(:username) { |n| "user-#{n}" }
sequence(:email) { |n| "user-#{n}#example.com" }
password "password"
password_confirmation "password"
factory :user_with_stories do
ignore do
stories_count 5
end
after(:create) do |user, evaluator|
create_list(:story, evaluator.stories_count, user: user)
end
end
end
end
This doesn't seem to work though - when I get into the console and run FactoryGirl.create(:user_with_stories).stories.length, I get an empty array. Am I missing something?
you should use after :build and build_list and assign the list to the user's association, like so:
after(:build) do |user, evaluator|
user.stories << build_list(:story, evaluator.stories_count, user: user)
end
Stories are added after creation of user, you need to instantiate the user after creation to see stories attached :
user = FactoryGirl.create(:user_with_stories)
user = User.find user
user.stories.length
Is there a way to have multiple versions of the same factory? For example, a user factory.
FactoryGirl.define do
factory :user#1 do
name 'John Doe'
date_of_birth { 21.years.ago }
end
factory :user#2 do
name 'Jane Doe'
date_of_birth { 25.years.ago }
end
end
is there something like this, so that I could call FactoryGirl.create :user#1 for John or FactoryGirl.create :user#2 for Jane?
The user factory is an example I don't actually use but my real factory is using a lot of data. I find it cumbersome to manually change a lot of data everytime I need another user.
You do not have to declare two times
just follow
FactoryGirl.define do
factory :user do
name
date_of_bith
end
end
Now Dynamically you can call
user1 = FactoryGirl.create(:user, name: 'John doe', date_of_birth: 21.year.ago)
user2 = FactoryGirl.create(:user, name: 'Jane doe', date_of_birth: 25.year.ago)
As opposed to providing all the data in the call, I just make my factories dynamic by using sequences to give me unique data.
FactoryGirl.define do
factory :mail_user do
mail_domain
account
sequence :userid do |n|
"mailuser-#{n}"
end
passwd "This!sADecentPassword11212"
active :true
filter :false
end
end
or by using traits / inheritance to give me the exact user data I need
FactoryGirl.define do
factory :administrator do
isp
sequence :email do |n|
"user#{n}#example.com"
end
active true
name "Random Awesome Admin"
password '123Abc#$1'
password_confirmation '123Abc#$1'
trait :inactive do
active false
end
trait :system do
roles [:system_admin]
end
trait :admin do
roles [:admin ]
end
trait :realm do
roles [:realm_admin]
end
trait :helpdesk do
roles [ :helpdesk ]
end
trait :dns do
roles [ :dns_admin ]
end
factory :inactive_administrator, traits: [:inactive]
factory :helpdesk_admin, traits: [:helpdesk]
factory :system_admin, traits: [:system]
factory :admin, traits: [:admin]
factory :realm_admin, traits: [:realm]
factory :dns_admin, traits: [:dns]
end
end
Since in most cases I don't care about the actual username, or the like. just that it passes validation. So I would look at sequences/traits/dependent attributes. So that you rely less on hardcoded data.
You can generate dynamic factories by using sequence
factory :user do
sequence(:name) { |n| "person#{n}" }
sequence(:date_of_birth) { |n| "#{n}".years.ago }
end
And if you want to generate birth date based on your validation, your can override the initial value used for sequence
sequence(:date_of_birth, 15) { |n| "#{n}".years.ago }