Duplicate associations in Factory Girl - ruby-on-rails

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.

Related

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

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

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

Best practice for reusing code in Rspec?

I'm writing integration tests using Rspec and Capybara. I've noticed that quite often I have to execute the same bits of code when it comes to testing the creation of activerecord options.
For instance:
it "should create a new instance" do
# I create an instance here
end
it "should do something based on a new instance" do
# I create an instance here
# I click into the record and add a sub record, or something else
end
The problem seems to be that ActiveRecord objects aren't persisted across tests, however Capybara by default maintains the same session in a spec (weirdness).
I could mock these records, but since this is an integration test and some of these records are pretty complicated (they have image attachments and whatnot) it's much simpler to use Capybara and fill out the user-facing forms.
I've tried defining a function that creates a new record, but that doesn't feel right for some reason. What's the best practice for this?
There are a couple different ways to go here. First of all, in both cases, you can group your example blocks under either a describe or context block, like this:
describe "your instance" do
it "..." do
# do stuff here
end
it "..." do
# do other stuff here
end
end
Then, within the describe or context block, you can set up state that can be used in all the examples, like this:
describe "your instance" do
# run before each example block under the describe block
before(:each) do
# I create an instance here
end
it "creates a new instance" do
# do stuff here
end
it "do something based on a new instance" do
# do other stuff here
end
end
As an alternative to the before(:each) block, you can also use let helper, which I find a little more readable. You can see more about it here.
The very best practice for your requirements is to use Factory Girl for creating records from a blueprint which define common attributes and database_cleaner to clean database across different tests/specs.
And never keep state (such as created records) across different specs, it will lead to dependent specs. You could spot this kind of dependencies using the --order rand option of rspec. If your specs fails randomly you have this kind of issue.
Given the title (...reusing code in Rspec) I suggest the reading of RSpec custom matchers in the "Ruby on Rails Tutorial".
Michael Hartl suggests two solutions to duplication in specs:
Define helper methods for common operations (e.g. log in a user)
Define custom matchers
Use these stuff help decoupling the tests from the implementation.
In addition to these I suggest (as Fabio said) to use FactoryGirl.
You could check my sample rails project. You could find there: https://github.com/lucassus/locomotive
how to use factory_girl
some examples of custom matchers and macros (in spec/support)
how to use shared_examples
and finally how to use very nice shoulda-macros
I would use a combination of factory_girl and Rspec's let method:
describe User do
let(:user) { create :user } # 'create' is a factory_girl method, that will save a new user in the test database
it "should be able to run" do
user.run.should be_true
end
it "should not be able to walk" do
user.walk.should be_false
end
end
# spec/factories/users.rb
FactoryGirl.define do
factory :user do
email { Faker::Internet.email }
username { Faker::Internet.user_name }
end
end
This allows you to do great stuff like this:
describe User do
let(:user) { create :user, attributes }
let(:attributes) { Hash.new }
it "should be able to run" do
user.run.should be_true
end
it "should not be able to walk" do
user.walk.should be_false
end
context "when user is admin" do
let(:attributes) { { admin: true } }
it "should be able to walk" do
user.walk.should be_true
end
end
end

Resources