Defining a "child" variables in "let" - ruby-on-rails

I have 2 models which I want to test: Article and User. User has_many articles. In one of the test cases I define a variable:
context 'some context' do
let(:my_user) { FactoryGirl.create(:user) }
How do I create 100 articles belonging to that my_user? I tried this:
let(:my_user) { FactoryGirl.create(:user, articles: [build(:article)]) } # error
# or
let(:article) { FactoryGirl.create(:article, user: my_user) } # doesn't work
it 'fdsfdsfs' do
# my_user.articles still has no articles
end
but it didn't pan out.

You can either do it in the factory itself using callbacks or you can do it using create_list like so:
let!(:user) { FactoryGirl.create(:user) }
let!(:article) { FactoryGirl.create_list(:article, 100, user: user) }
I'm using let! here which might not be necessary but because you're not showing your entire test I can't be sure if everything is being lazy loaded.
To do it with callbacks, I like to do something like this:
FactoryGirl.define do
factory :user do
sequence(:email) { |n| "user#{n}#email.com" }
password 'password'
factory :user_with_articles do
after(:create) do |user|
create_list(:article, 100, user: user)
end
end
end
end
Now in your test you can do
let(:user) { FactoryGirl.create(:user_with_articles) }
This will create a user with 100 articles.

As a complement to the fine answer https://stackoverflow.com/a/27756067/1008891, it appears that your first attempt failed because you tried to invoke FactoryGirl.build without qualifying the reference to build and your second attempt failed because let(:article) was lazily evaluated and you never referenced article in your test, resulting the article never getting associated with your user.

Related

Why my factory test raises an error from another factory?

I'm quite new with Rspec and factory_bot. I have the following factories:
FactoryBot.define do
factory :user do
email { "test#gmail.com" }
password { "azerty" }
username { "test" }
city { "Paris"}
end
factory :game do
title { "Mega Man X" }
nostalgia_point { 9 }
platform { "Super Nintendo" }
developer { "Capcom" }
release_date { Date.new(1994,1,1) }
game_mode { "Single player" }
genre { ["Adventure", "Platform", "Shooter"] }
association :owner, factory: :user
end
factory :favorite do
user
game
end
end
When I run this test:
require 'rails_helper'
RSpec.describe Game, type: :model do
let!(:favorite) { create(:favorite) }
describe '::create' do
context 'persistence' do
it 'persists the favorite' do
expect(Favorite.count).to eq(1)
end
end
end
end
I get the following error: ActiveRecord::RecordInvalid: Validation failed: Email has already been taken, Username has already been taken
I guess it's because my user factory (devise model) is not cleaned from my other tests but I'm absolutely not sure... And I don't know if I should use something different from what I did to clean all my database. Here what I wrote in factory_bot.rb:
RSpec.configure do |config|
config.include Warden::Test::Helpers
config.after :each do
Warden.test_reset!
end
config.include FactoryBot::Syntax::Methods
end
If there is a better practice or just tell me what I'm missing, I'll be very grateful. Thanks!
There must be a validation in your user model that prevents you from creating users with an email and/or username that's already in the database. And as you have a factory (favorite), that attempts to create a user and a game, without checking first if the user email and/or username already exist, it fails because it uses the values you set for them, which are always the same ("test#gmail.com", "test", correspondingly).
You can use a FactoryBot sequence for that:
sequence :email do |n|
"test#{n}#gmail.com"
end
sequence :username do |n|
"test-#{n}"
end
That prevents you from using the same value for new records as sequence yields an incremental integer, hence the value is guaranteed to not be the same as long as the test suite runs.

Why is 'FactoryGirl.lint' giving InvalidFactoryError?

Long time reader but first time poster here on SO :)
For the last couple of days I've been setting up FactoryGirl.
Yesterday I changed some factories (mainly my User and Brand factories) by replacing:
Language.find_or_create_by_code('en')
With:
Language.find_by_code('en') || create(:language)
Because the first option creates a Language object with only the code attribute filled in; while the second uses the Language factory to create the object (and thus fills in all the attributes specified in the factory)
Now when I run my test it immediately fails on Factory.lint, stating my user (and admin_user) factories are invalid. Reverting the above code doesn't fix this and the stack trace provided by FactoryGirl.lint is pretty useless..
When I comment the lint function, my tests actually run fine without any issues.
When I manually create the factory in rails console and use .valid? on it, it returns true so I'm at a loss why lint considers my factories invalid.
My user factory looks like this:
FactoryGirl.define do
factory :user do
ignore do
lang { Language.find_by_code('en') || create(:language) }
end
login "test_user"
email "test_user#test.com"
name "Jan"
password "test1234"
password_confirmation "test1234"
role # belongs_to :role
brand # belongs_to :brand
person # belongs_to :person
language { lang } # belongs_to :language
factory :admin_user do
association :role, factory: :admin
end
end
end
Here the role, person and language factories are pretty straightforward (just some strings) but the brand factory shares the same language as the user thus I use the code in the ignore block so FactoryGirl doesn't create 2 'en' language entries in my database.
Anyone has some ideas why I'm getting this InvalidFactoryError and maybe provide some insights on how to debug this?
UPDATE 1
It seems this problem is caused by another factory..
I have a factory called user_var_widget where I link a specific widget with a user:
factory :user_solar_widget, :class => 'UserWidget' do
sequence_number 2
user { User.find_by_login('test_user') } # || create(:user) }
widget { Widget.find_by_type('SolarWidget') || create(:solar_widget) }
end
If I uncomment the create(:user) part, I get InvalidFactoryError for the User factory. My guess is because there is nothing in the User factory that states it has any user_widgets. I will experiment a bit with callbacks to see if I can resolve this.
UPDATE 2
I've managed to solve this by adding this to my User factory:
trait :with_widgets do
after(:create) do |user|
user.user_widgets << create(:user_solar_widget, user: user)
end
end
Where user_widgets is a has_many association in the user model.
Then I changed my user_solar_widget factory to:
factory :user_solar_widget, :class => 'UserWidget' do
sequence_number 2
# removed the user line
widget { Widget.find_by_type('SolarWidget') || create(:solar_widget) }
end
I then create a user by calling:
create :user, :with_widgets
Still, it would have been nice if the lint function was a bit more specific about invalid factories..
FactoryGirl.lint is almost useless because of it's non-existent error messages. I recommend instead including the following test:
# Based on https://github.com/thoughtbot/factory_girl/
# wiki/Testing-all-Factories-(with-RSpec)
require 'rails_helper'
RSpec.describe FactoryGirl do
described_class.factories.map(&:name).each do |factory_name|
describe "#{factory_name} factory" do
it 'is valid' do
factory = described_class.build(factory_name)
expect(factory)
.to be_valid, -> { factory.errors.full_messages.join("\n") }
end
end
end
end

How can I DRY up my Rspec/Capybara suite?

In my suite I have this in many it blocks:
let(:user) { create(:user) }
let(:plan) { Plan.first }
let(:subscription) { build(:subscription, user: user ) }
it "something" do
subscription.create_stripe_customer
subscription.update_card valid_card_data
subscription.change_plan_to plan
login_as user
end
How could I DRY this up so I don't have to duplicate all these lines across many files?
You can also create a method like
def prepare_subscription
subscription.create_stripe_customer
subscription.update_card valid_card_data
subscription.change_plan_to plan
end
And in your it block like so:
it "something" do
prepare_subscription
login_as user
end
You ain't checking value for that spec so it always green.
If you need prepare some data before test then you could put that code into helper and call it when needed in (for example) before block.
If you need check spec passing again and again then you could use shared examples.

My factories are being autogenerated when I do `rake db:test:prepare`

When I run rake db:test:prepare,
It automagically generates my Factories :
require 'ffaker'
FactoryGirl.define do
factory :user do
sequence(:email) {|i| "marley_child#{i}#gmail.com" }
password 'secret_shhh'
end
factory :brain do
user FactoryGirl.create :user
end
end
And then if I try to run rspec or even access my console with rails c test, I get a validation error :
/activerecord-3.2.6/lib/active_record/validations.rb:56:in `save!': Validation failed: Email has already been taken (ActiveRecord::RecordInvalid)
My Rspec :
describe '#email' do
context 'uniqueness' do
let(:user) { FactoryGirl.build :user, email: 'Foo#Bar.COM' }
subject { user.errors }
before do
FactoryGirl.create :user, email: 'foo#bar.com'
user.valid?
end
its(:messages) { should include(email: ['has already been taken']) }
end
end
What makes no sense to me is I assumed this data was transactional. Why are my factories getting generated when I prepare by data and not within each test? What is the most appropriate way to do this?
Well, one problem is that in your :brain factory definition, you're actually calling FactoryGirl.create :user as part of the definition of the factory when you presumably meant to call it when the factory is invoked (i.e. user {FactoryGirl.create :user}).
As for why there is already a User in the database, I can't answer that except to say that sometimes even if you're running with transactions turned on and things go south, records can be left behind.

Getting Rspec2 Errors On Working Rails Code

I just figured out how to display a country name from a country id number on model User. Here is the basic code I am using in a controller and two views to display the name after finding User by its id:
#country_name = Country.find(#user.country_id).name
I am using Factory Girl to simulate user records where the default for country_id is 1 for the United States. Before I added this logic my Rspec2 tests were clean. Now I get the following error when I run my tests. Every test which has similar logic in the view or controller produces the error.
ActiveRecord::RecordNotFound:
Couldn't find Country with id=1
Here are two of the Rspec2 tests I am doing:
describe "profile page" do
let(:user) { FactoryGirl.create(:user) }
before { visit user_path(user) }
it { should have_selector('span', text: user.first_name+' '+user.last_name) }
it { should have_selector('title', text: user.first_name+' '+user.last_name) }
end
The profile page references #country_name which is set as stated above. The country name displays on the screen as expected.
I'm thinking that I need to add something to the Rspec2 tests somehow. I was not sure where to do this. Since #country_name is an instance variable not related to User I felt that maybe I needed to do something directly in my Rspec2 file.
Any help would be appreciated. I have not found anything like this so far but I will continue looking.
Update 6/8/2012 7:27 pm CDT
Here is my user controller logic. Adding the second line here produced the RSpec2 errors. I changed nothing in the RSpec2 tests. #country_name is referenced in show.html.erb.
def show
#user = User.find(params[:id])
#country_name = Country.find(#user.country_id).name
end
I decided to try this but got the same error:
describe "profile page" do
let(:user) { FactoryGirl.create(:user) }
let(:country) { FactoryGirl.create(:country) }
before { visit user_path(user) }
it { should have_selector('span', text: user.first_name+' '+user.last_name) }
it { should have_selector('title', text: user.first_name+' '+user.last_name) }
end
I added the following to factories.rb separate from the user factory.
factory :country do
id 1
name "CountryName"
iso "CN"
end
Again my application logic for the country name is working wonderfully.
Update 6/11/2012 9:09 am CDT
Here is my factories.db file. country_id has a default value of 1 in User. I tried this with country_id defined as below and without a declaration for country_id. I also had the country factory without the id. I still got the errors as described. I tried this before submitting my question here.
FactoryGirl.define do
factory :user do
sequence(:first_name) { |n| "First Name #{n}" }
sequence(:last_name) { |n| "Last Name #{n}" }
sequence(:username) { |n| "Username#{n}" }
sequence(:email) { |n| "person_#{n}#example.com" }
password "foobar"
password_confirmation "foobar"
sequence(:city) { |n| "City #{n}" }
sequence(:state_province) { |n| "State/Province #{n}" }
active_user "1"
country_id 1
factory :admin do
admin true
active_user "3"
end
factory :blocked do
active_user "1"
end
factory :rejected do
active_user "2"
end
factory :not_visible do
active_user "3"
visible false
end
factory :visible do
active_user "3"
visible true
end
end
factory :country do
id 1
name "CountryName"
iso "CN"
end
end
Update 6/11/2012 5:37 pm CDT
After several hours of continuing to search AND COMPLETELY REBOOTING MY COMPUTER (sigh) I was finally able to get the tests to work. I replaced country with user.association :country in the user factory. I put my original two statements after the describe "profile page" do statement back. With the suggested changes I now have no errors. I guess the association will create corresponding rows in Country.
Thanks so much for all the help. It was a combination of the help received with one modification that solved this one.
On to figuring out how to check for changed values.......
Update 6/12/2012 10:55 am
It looks like with all the correcting of code I am able to use #user.country.name with no errors. I will change my code to use this streamline coding.
Let is more for setting up variables for use in the test, if you're using database cleaner or something similar, these are probably getting destroyed in the database on each test run. You should be setting up your factories in the before block:
Edit: I've just realised this is probably because you're trying to match records by explicitly entering ids. This won't work (ids are automatically set by the database), but Factory Girl should deal with your associations, see what happens when you set up the before block like this:
before(:each) do
country = FactoryGirl.create(:country)
user = FactoryGirl.create(:user, country: country)
visit user_path(user)
end
I would also remove the
id 1
line from your country factory, and the line setting up the country_id to 1 on the user factory.
Edit 2: You need to tell Factory Girl about your associations, don't try to set them up by explicitely setting the id field because that won't work.
Remove
country_id 1
from the user factory
id 1
from the country factory, and add this line to the user factory:
country
(yes, just country on it's own - Factory Girl is clever enough to figure out what's going on from that as long as the factory and association is named the same)
By the way, you could change this:
#country_name = Country.find(#user.country_id).name
to
#country_name = #user.country.name
It won't help here, but it's cleaner.

Resources