Why is 'FactoryGirl.lint' giving InvalidFactoryError? - ruby-on-rails

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

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.

Duplicate associations in Factory Girl

I am using fixtures to load test data on a Ruby on Rails project. I moved to Factory Girl, but I am getting duplicate entries for associations.
I have a Group model and a Value model. A Group can have multiple Values.
Also, I am using Cucumber for my tests. And FactoryGirl.lint to populate the database.
My fixtures
groups.yml
group_1:
name: "Flavour"
values.yml
value_1:
name: "Strawberry"
group: group_1
value_2:
name: "Mint"
group: group_1
value_3:
name: "Chocolate"
group: group_1
This works just fine. A single Group is created and the 3 Values are attached to it.
My factories
groups.rb
FactoryGirl.define do
factory :group do
name "Flavour"
end
end
values.rb
FactoryGirl.define do
factory :value_1, class: :value do
name "Strawberry"
group
end
factory :value_2, class: :value do
name "Mint"
group
end
factory :value_3, class: :value do
name "Chocolate"
group
end
end
This is not working. Factory Girl is creating 3 Groups, each one associated to 1 Value.
Is this normal behaviour for Factory Girl?
Also, I read that use_transactional_fixtures should be set to true. This is already the case.
I don't think you really have grasped the conceptual differences between using fixtures and factories.
With fixtures you have these static object definitions that get chucked into the database on each test run. Its like your database has a "zero" state with a bunch of data already. Feels nice, warm and fuzzy (Oh I don't have to set everything up, so nice!) but its a horrible idea in reality since it masks any errors caused by an empty table!
With factories you define objects dynamically instead and create them when needed. You use a tool like database_cleaner to clean out any residual state between tests. You start each test with nothing.
FactoryGirl.lint does not populate the database. Its a linter that checks that your factory definitions are correct.
There is absolutely zero point in using factory_girl if you just create a bunch of fixtures with it. So instead you want to do something like:
FactoryGirl.define do
factory :value do
name { ["Strawberry", "Mint", "Chocolate"].sample }
group
end
end
One of the mental hurdles of going from fixtures to factories is you have to stop writing tests like if you where using fixture data:
describe "GET /users/:id" do
let!(:user){ FactoryGirl.create(:user) }
let(:json) { JSON.parse(response.body) }
before { get users_path(#user), format: :json }
it "has the correct name" do
# bad
expect(json["name"]).to eq "John Doe" # strong coupling to factory!
# good
expect(json["name"]).to eq user.name # we don't know what the factory generates.
end
end
If you have to have a test where a certain factory value must be known or if a factory needs to be associated to a certain object then do it explicitly instead of making a mess of your definitions.
let(:user) { FactoryGirl.create(:user, name: 'Max') }
let(:item) { user.items.create(FactoryGirl.attributes_for(:item)) }
it "has an awesome name" do
expect(user.name).to eq 'Max'
end
it "is associated" do
expect(item.user).to eq user
end
Your value factories are told to use the group factory for their group, and the group factory is told to make a new group with name 'Flavor'. What you want to do differently is that you want the group factory to return the existing 'Flavor' rather than a new one. Here is how to do that.
FactoryGirl.define do
factory :group, class: :group do
name 'Flavor'
initialize_with {Group.find_or_initialize_by(name: name)}
end
end
That is saying if you already have a group with desired name, use existing group. Written as above so that there is a default name, and you get the same 'use existing if present' behavior when passing in an alternate name.

Rspec/FactoryGirl uniqueness validations in a large test suite

Using Rspec and FactoryGirl, if I have a factory that autoincrements a trait using a sequence, and in some specs if I explicitly set this trait, with a large enough test suite, sometimes random specs fail with
Validation failed: uniq_id has already been taken
The factory is defined like this:
factory :user { sequence(:uniq_id) {|n| n + 1000} }
I'm guessing this validation fails because in one place in my test suite, I generate a user like this:
create(:user, uniq_id: 5555)
And because presumably factory girl is generating more than 4,555 users over the suite, the validation is failing?
I'm attempting to avoid this problem by just turning the uniq_id into 55555 (larger number), so there is no interference. But is there a better solution? My spec_helper includes these relevant bits:
config.use_transactional_fixtures = true
config.after(:all) do
DatabaseCleaner.clean_with(:truncation)
end
It happens to me sometimes. I didn't found any explanation, but happens only with big set of data. I let someone find the explanation!
When it happens, you can declare your attribute like this (here is an example using faker gem) :
FactoryGirl.define do
factory :user do
login do
# first attempt
l = Faker::Internet.user_name
while User.exists?(:login => l) do
# Here is a loop forcing validation
l = Faker::Internet.user_name
end
l # return login
end
end
end
I was able to solve my issue like this in my factory (based on #gotva's suggestion in the question comments).
factory :user do
sequence(:uniq_id) { |n| n + 1000 }
# increment again if somehow invalid
after(:build) do |obj|
if !obj.valid? && obj.errors.keys.include?(:uniq_id)
obj.uniq_id +=1
end
end
end

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.

File upload, factory_girl & database_cleaner

In my model, I have to choose an asset, saved in a editorial_asset table.
include ActionDispatch::TestProcess
FactoryGirl.define do
factory :editorial_asset do
editorial_asset { fixture_file_upload("#{Rails.root}/spec/fixtures/files/fakeUp.png", "image/png") }
end
end
so I have attached in my model factory an association on :editorial_asset
Upload work great, but take too much time (1s per example)
I'm wonder if it's possible to create uploads one time before each examples, and say in the factory: "find instead of create"
But the problem with database_cleaner, I cannot except tables with :transaction, truncation take 25sec instead of 40ms !
EDIT
The factory that need an asset
FactoryGirl.define do
factory :actu do
sequence(:title) {|n| "Actu #{n}"}
sequence(:subtitle) {|n| "Sous-sitre #{n}"}
body Lipsum.paragraphs[3]
# Associations
user
# editorial_asset
end
end
The model spec
require 'spec_helper'
describe Actu do
before(:all) do
#asset = create(:editorial_asset)
end
after(:all) do
EditorialAsset.destroy_all
end
it "has a valid factory" do
create(:actu).should be_valid
end
end
So a working way is
it "has a valid factory" do
create(:actu, editorial_asset: #asset).should be_valid
end
but there's no way to inject automatically association ?
Since you're using RSpec, you could use a before(:all) block to set up these records once. However, anything done in a before-all block is NOT considered part of the transaction, so you will have to delete anything from the DB yourself in an after-all block.
Your factory for the model that has an association to the editorial asset could then, yes, try to first find one before creating it. Instead of doing something like association :editorial_asset you could do:
editorial_asset { EditorialAsset.first || Factory.create(:editorial_asset) }
Your rspec tests could then look like this:
before(:all) do
#editorial = Factory.create :editorial_asset
end
after(:all) do
EditorialAsset.destroy_all
end
it "already has an editorial asset." do
model = Factory.create :model_with_editorial_asset
model.editorial_asset.should == #editorial
end
Read more about before and after blocks on the Rspec GitHub wiki page or on the Relish documentation:
https://github.com/rspec/rspec-rails
https://www.relishapp.com/rspec

Resources