The Factory Girl docs offer this syntax for creating (I guess) parent-child associations...
Factory.define :post do |p|
p.author {|a| a.association(:user) }
end
A post belongs to a User (its "author").
What if you want to define a Factory to create Users, that have a bunch of Posts?
Or what if it's a many-to-many situation (see update below for example)?
UPDATE
I thought I had figured it out. I tried this...
Factory.define(:user) do |f|
f.username { Factory.next(:username) }
# ...
f.roles { |user|
[
Factory(:role),
Factory(:role, {:name => 'EDIT_STAFF_DATA'})
]
}
end
It seemed to work at first, but then I got validation errors because F.G. was trying to save the user twice with same username and email.
So I return to my original question. If you have a many-to-many relationship, like say Users and Roles, how can you define a Factory that will return Users with some associated Roles? Note that Roles must be unique, so I can't have F.G. creating a new "ADMIN" Role in the DB every time it creates a User.
I'm not sure if this is the most correct way of doing it, but it works.
Factory.define(:user) do |u|
u.login 'my_login'
u.password 'test'
u.password_confirmation 'test'
u.roles {|user| [user.association(:admin_role),
user.association(:owner_role, :authorizable_type => 'User', :authorizable_id => u.id) ]}
end
A recent update to factory girl allows associations to be specified with callback blocks
I created an active_factory plugin, that deals with your situation in spec like this:
models { user - posts(3) }
If there is an interest, i may try to create integration with factory_girl factories.
Related
I am getting a (frustrating!) error when running rspec (with Factory Bot) tests for my Rails app. I'm seeing the following error:
ActiveRecord::NotNullViolation:
Mysql2::Error: Field 'group_id' doesn't have a default value: INSERT INTO `groups_users` VALUES ()
It is intermittent - different tests will throw the error on different runs - which might suggest a race condition?
My models are configured as:
User habtm Groups
Groups habtm Users
Staff members MUST belong to one group - with a validation of:
validates :groups, presence: { message: 'Staff must be in at least one group' }, if: :staff?
Users
factory :a_staff_member, class: User do
first_name { Faker::Name.first_name }
last_name { Faker::Name.last_name }
username { "#{first_name}_#{last_name}".downcase.gsub(/[^a-z]/i, '') }
email { "#{first_name}.#{last_name}#example.ac.uk".downcase }
user_type { :staff }
groups {|s| [s.association(:group)]}
end
Groups
FactoryBot.define do
factory :group do
sequence(:title) { |n| "Group Number #{n}" }
sequence(:short_title) { |n| "GRP#{n}" }
sequence(:url_slug) { |n| "grp#{n}" }
end
end
The common factor in all of the failing tests is the User creation factory (and the Groups creation given a User MUST belong to a group) - so this is where I have been focussing my efforts.
Given that the error is on the groups_users table I'm guessing that it's trying to save an entry to the join table before the group exists in the groups table?
Based on other SO posts and trawling the Internet, I've tried various ways of setting the group:
1) groups {|s| [s.association(:group)]}
2) after(:build) {|user| user.groups = [create(:group)]}
3) groups {[FactoryBot.create(:group)]}
The users are just instantiated via
let!(:staff_user) { FactoryBot.create(:a_staff_member) }
in the individual specs - so nothing exciting there.
However, no mater how I create the groups in the factories it doesn't make any difference - and I've run out of ideas - any pointers or suggestions would be gratefully received!
I think trying to change the equality with concatenation might work.
I'd add after(:create) { |user| user.groups << create(:group) }
Moving to after(:create) is the main thing here as the problem you are facing also seems to be trying to add a join table record for a record that is not created yet.
Managed to track down the issue. After putting some comprehensive debug messages around the point in the factory that the group was created and assigned to the user, it became clear that the error message relating to nil values in the join table between these was, in fact, a red herring and being caused outside of the factory.
I believe the issue was caused by the user session/log in not being cleaned up between tests from Capybara.
Adding:
config.after :each do
Warden.test_reset!
end
to /spec/spec_helper.rb appears to have resolved the issue. Hopefully the hair pulled out during this investigation will grow back!
I think that it might work if you change like this.
from
after(:build) {|user| user.groups = [create(:group)]}
to
after(:create) {|user| user.groups = [create(:group)]}
Once a user id is created by running "create", then "users.groups" will be acceptable.
I have two simple factories: One for user and one for blog. When creating a blog, it must have an associated user, so this is reflected in the blog factory:
FactoryGirl.define do
factory :user do
end
end
FactoryGirl.define do
factory :blog do
user #specifying the required association
end
end
Now I can create a blog in a spec like so:
blog = create(:blog)
what I am not sure of how to do in the spec is how to specify some custom attributes on the associated user record immediately when the blog record is first created.
For example: when creating the blog record (which also creates an associated user record), I want to specify the first_name attribute on that associated user record. Something like:
blog = create(:blog, associated_user: (first_name: 'foobar) )
That syntax up above doesn't work but I want to do something like that. I did look at the factory girl docs, and I have been googling but have been unable to figure this one out.
Question: How do I set some custom attributes on an associated object when creating an object with factory girl?
You can simply set the corresponding call argument with the needed Object.
user = create(:user, first_name: "Harald")
blog = create(:blog, user: user)
# or inline if preferred
blog = create(:blog, user: create(:user, first_name: "Heinz"))
Note: Don't forget to rather use build than create wherever possible to speedup your tests :)
Is it possible to use an association as the value for another association in a trait?
Let me describe a (simplified) example. I have 3 models
User
Forum (belongs_to :forum)
Thread (belongs_to :forum, belongs_to :user)
Please note that Thread contains an user_id, it does not belong to User through Forum (at the time being, I can't change such constraint).
What I'd like to know, is if there a way to define a trait like the one
FactoryGirl.define do
factory :thread do
trait :with_forum do
association :user
# Lazy-evaluate the value of user to the previously created factory
# THIS IS THE KEY POINT: I want 'user' to be the factory created at the previous line
association :forum, user: user
end
end
end
What the trait is supposed to do is to create an user and associate it to the thread.
Then it should create a forum, but the user should be the same instance previously created.
There are mainly two reasons:
The user associated with the forum is the same of the thread
I don't want to create two user factories on cascade
Any idea? I tried to use the lazy evaluation, but no way to use it with associations.
Does it make sense to do the opposite and set the thread's user to be the same as the forum's? Does the forum factory create a user?
FactoryGirl.define do
factory :thread do
trait :with_forum do
forum
user { forum.user }
end
end
end
If you really want to do it the other way, you can definitely use a lazy attribute:
FactoryGirl.define do
factory :thread do
trait :with_forum do
user
forum { create(:forum, user: user) }
end
end
end
The downside there will be that it will always create a forum regardless of the strategy used to build the thread.
You have to lazily evaluate the association call:
FactoryGirl.define do
factory :thread do
trait :with_forum do
user
forum { association(:forum, user: user) }
end
end
end
If this is the only line I have creating a user in my seeds.rb file:
jim = Factory :user, :name => "Jim Jones", :email => 'jim#test.com'
How come I end up with two users, one named Jim, and one with a random name as if it was just generated by the factory?
Thanks!
John
UPDATE: The reason, I realize now, is because another factory is being created which has an association to user, and I don't specify which user, and so it would seem the factory with an association to user is generating a user.
I'm assuming you defined Factory :user somewhere? That's the general use case for Factory anyways. With that defined calling Factory :user will return whatever it was defined to do before. Have a look at the Getting Started With Factory_Girl page.
You should be able to call FactoryGirl.create(:user) from there.
Good luck!
Trying to wrap my head around rspec and proper testing and having some hard time to do the following properly
Let's say we have three classes like
Class User
belongs_to :company
has_and_belongs_to_many :roles
end
Class Company
has_many :users
end
Class Role
has_and_belongs_to_many :users
end
In the User class I have before_create callback that assign the user default 'company_admin' role, if the user is first one to be associated with the company
def check_for_initial_company_admin_role
if self.company.users.count == 0
self.roles << Role.find_by_name("company_admin")
end
end
How do I properly test in my model spec that the user gets assigned the 'company_admin' role in case he's the first user associated with the company?
UPDATE
working solution
describe "#check_for_initial_company_admin_role" do
it "sets the first user created to be the administrator" do
Factory(:role)
user = Factory(:user)
user.roles.count.should be > 0
user.roles.should include Role.find_by_name("company_admin")
end
end
Factory.define :user do |f|
f.email { Factory.next(:email) }
f.password "secret"
f.password_confirmation "secret"
f.association :company
end
Factory.define :role do |f|
f.name "company_admin"
end
Factory.define :company do |f|
f.name "foobar"
f.vat_id "1234"
end
I would approach it like this:
describe "#check_for_initial_company_admin_role" do
it "sets the first user created to be the administrator" do
company = Factory(:company)
user = Factory(:user)
company.users << user
user.roles.count.should > 0
user.roles.should include Role.find_by_name("company_admin")
end
end
An assumption here that may be incorrect is that you are using Factory Girl in your test framework. If not, that doesn't really change the "meat" of this test...just those first lines where you create the company and user.
You could also optionally check the user from the company side of things but honestly that feels like a different test entirely--one testing the association between those models.
The approach I would take is that since this is actually a model test you need to create and alter a real model objects, rather than mocking out those objects. If this were a controller test, I'd mock the models and stub the models aggressively.
I hope this helps and addresses your question. If not, let me know where I'm off base and I'll make another pass at it :) I'm only about a year into rspec but I've found that once I wrapped my head around how to test models vs. controllers I've come to love it.
Without changing your existing logic, I would test this logic in the user_spec like so:
describe User do
let!(:admin_role) { Role.create!(name: 'company_admin') }
let!(:company) { Company.create! }
it 'should be added to the default role when created' do
user = company.users.create!(name: 'Joe', email: 'joe#email.com')
user.should have(1).roles
user.roles.first.should == admin_role
end
end
Note: I would be using something like FactoryGirl for the admin role and company objects to make them reusable.
Your use of the role name to indicate behavior is not ideal. It will likely lead to lots of scattered logic throughout your application where you are finding the role by its name and checking the name with if/else or case statements. I would recommend using Rail's single table inheritance and moving all the admin role logic to a separate class. It will keep the model's logic cleaner and make testing much easier.