How to make has_many :through association with fixtures? - ruby-on-rails

I can't use factory_girl because I'm testing sunspot and need real database.
Edit: nope. It can works with sunspot. I'm wrong.
How can I build has_many :through(a.k.a many-to-many) associations in fixtures?
I google it and get a invalid solution
Edit:
Finally I use factory_girl. I google-copy-paste a snippet:
factory :tagging do
question { |a| a.association(:question) }
tag { |a| a.association(:tag) }
end
(question has_many tags through taggings, vice versa)
It works well. But what's it? The factory_girl's readme didn't meantion this syntax.
Could someone explain?

You can find the official documentation for factory_girl, which is very complete, here.
Here is a nice (shorter) blogpost explaining factory_girl 2 (comparing it with factory-girl 1).
UPDATED:
To Explain the code a bit:
factory :tagging do
association :tag
end
will look for a factory called :tag and will construct that object, and then link that to the association tag (e.g. a belongs_to) that is there inside your object :tagging.
Please note: this is the default factory. If you want taggings to share a tag, you will need to do something like
#tag = Factory(:tag)
#tagging_1 = Factory(:tagging, :tag => #tag)
#tagging_2 = Factory(:tagging, :tag => #tag)
Hope this helps.

If it's a classic has_and_belongs_to_many association, without other information in the association model, I think the conventions allow you to write your fixtures like that :
#users.yml
john:
first_name: John
last_name: Doe
hobbies: [swim, play_tennis]
#hobbies.yml
swim:
name: Swim
play_tennis:
name: Play Tennis
But I'm not completely sure !

I used fixtures on testing for has_many :through by hash merge
# posts.yml
one:
title: "Railscasts"
url: "http://railscasts.com/"
description: "Ruby on Rails screencasts"
# categories.yml
one:
name: "magazine"
two:
name: "tutorial"
three:
name: "news"
four:
name: "Ruby"
# posts_controller_test.rb
def test_post_create
assert_difference 'Post.count' do
post :create, post: posts(:one).attributes
.merge(categories: [categories(:two), categories(:four)])
end
end
when after adding another fixture file, and tried this it didn't work
# post_categories.yml
one:
post: one
category: two
two:
post: one
category: four
def test_post_create
assert_difference 'Post.count' do
post :create, post: posts(:one)
end
end
puts posts(:one).attributes
# {"id"=>980190962, "url"=>"http://railscasts.com/", "title"=>"Railscasts", "description"=>"Ruby on Rails screencasts", "created_at"=>Thu, 14 May 2015 18:27:20 UTC +00:00, "updated_at"=>Thu, 14 May 2015 18:27:20 UTC +00:00}
puts posts(:one).attributes
.merge(categories: [categories(:two), categories(:four)])
# {"id"=>980190962, "url"=>"http://railscasts.com/", "title"=>"Railscasts", "description"=>"Ruby on Rails screencasts", "created_at"=>Thu, 14 May 2015 18:30:23 UTC +00:00, "updated_at"=>Thu, 14 May 2015 18:30:23 UTC +00:00, "category_ids"=>[980190962, 1018350795]}

Related

Rails fixture associations

Struggling to get fixtures to associate. We are (finally!) writing tests for an existing app. We are using Minitest as the framework.
mail_queue_item.rb:
class MailQueueItem < ApplicationRecord
belongs_to :order
...
end
mail_queue_items.yml:
one:
run_at: 2015-01-04 10:22:19
mail_to: test#test.com
order: seven_days_ago
email_template: with_content
customer: basic_customer
status: waiting
orders.yml:
seven_days_ago:
tenant: basic_tenant
ecom_store: basic_store
ecom_order_id: 123-123456-123456
purchase_date: <%= 7.days.ago %>
set_to_shipped_at: <%= 6.days.ago %>
ecom_order_status: shipped
fulfillment_channel: XYZ
customer: basic_customer
In a test:
require 'test_helper'
class MailQueueItemDenormalizerTest < ActiveSupport::TestCase
fixtures :mail_queue_items, :customers, :email_templates, :orders
test 'should make hash' do
#mqi = mail_queue_items(:one)
puts #mqi.order_id.inspect
puts #mqi.order.inspect
order = orders(:seven_days_ago)
puts order.inspect
assert #mqi.order.ecom_order_status == 'shipped'
end
end
The output looks like this:
MailQueueItemDenormalizerTest
447558226
nil
#<Order id: 447558226, tenant_id: 926560165, customer_id: 604023446, ecom_order_id: "123-123456-123456", purchase_date: "2022-08-13 19:18:02.000000000 -0700", last_update_date: nil, ecom_order_status: "shipped", fulfillment_channel: "XYZ", ....>
test_should_make_hash ERROR (5.96s)
Minitest::UnexpectedError: NoMethodError: undefined method `ecom_order_status' for nil:NilClass
test/denormalizers/mail_queue_item_denormalizer_test.rb:26:in `block in <class:MailQueueItemDenormalizerTest>'
So even though the order_id on the mail_queue_item is correct (it matches the id from the object loaded from the fixture) the association does not work.
I have tried the suggestions in Nil Associations with Rails Fixtures... how to fix? of putting ids in everything and the result is the same.
Project is in Rails 6 (long project that started life in Rails 3.1).
The issue turned out to be that the fixtures were creating invalid objects. The objects were valid enough to get written to the database, but were not passing the Rails validations.
The resulting behavior is quite odd I think, but I don't know of a better way to do it.
I discovered this by adding:
puts "#mqi.order.valid? = #{#mqi.order.valid?}"
puts "#mqi.customer.valid? = #{#mqi.customer.valid?}"
puts "#mqi.email_template.valid? = #{#mqi.email_template.valid?}"
puts #mqi.email_template.errors.full_messages
code in there. Yes, it's disgusting.

How do I set an _id value in FactoryBot that isn't a foreign key?

I'm using FactoryBot (formerly FactoryGirl) to create some factory data for my tests. I have a model that looks like this via the schema (stripped down to just the relevant stuff):
create_table "items", force: :cascade do |t|
t.text "team"
t.text "feature_id"
t.text "feature"
t.timestamps
end
However, feature_id and feature are NOT references to a feature object... they are just strings.
I've defined my factory like this:
FactoryBot.define do
factory :item do
team "TheTeam"
sequence(:feature_id) {|n| "Feature#{n}" }
feature { Faker::Lorem.sentence }
end
end
And the simple case works:
> FactoryBot.create(:item)
=> #<Item:0x007fad8cbfc048
id: 1,
team: "TheTeam",
feature_id: "Feature1",
feature: "Qui voluptatem animi et rerum et.",
created_at: Wed, 10 Jan 2018 02:40:01 UTC +00:00,
updated_at: Wed, 10 Jan 2018 02:40:01 UTC +00:00>
But when I want to specify my own feature_id this is what happens:
> FactoryBot.create(:item, feature_id: "123")
=> #<Item:0x007fad8d30a880
id: 2,
team: "TheTeam",
feature_id: "123",
feature: nil,
created_at: Wed, 10 Jan 2018 02:40:59 UTC +00:00,
updated_at: Wed, 10 Jan 2018 02:40:59 UTC +00:00>
You can see that feature is now nil. I'm assuming this is because it's trying to infer that feature_id and feature are somehow related. But in this case, I don't want them to be.
Is there a better way to define the factory so that it just treats them as unrelated fields?
BTW, if I try to set both the feature_id and feature it looks like this:
> FactoryBot.create(:item, feature_id: "123", feature: "hi")
=> #<Item:0x007fad8d262810
id: 3,
team: "TheTeam",
feature_id: nil,
feature: nil,
created_at: Wed, 10 Jan 2018 02:45:01 UTC +00:00,
updated_at: Wed, 10 Jan 2018 02:45:01 UTC +00:00>
So it just sets both fields to nil. I suspect FactoryBot is trying to be "smart" about these fields based on their names. I'd change them but they are already set that way in the Db.
It does appear that FactoryBot is making assumptions, and I haven't found a way to change those assumptions. It might be worth opening an issue to see what the maintainers have to offer.
In the mean time, here's a workaround:
FactoryBot.define do
FEATURE_IDS ||= (1..1000).cycle
factory :item do
team "TheTeam"
transient { without_feature_id false }
transient { without_feature false }
after(:build, :stub) do |item, evaluator|
item.feature_id = "Feature#{FEATURE_IDS.next}" unless evaluator.without_feature_id
item.feature = Faker::Lorem.sentence unless evaluator.without_feature
end
end
end
This will function properly in the cases you described above.
The incrementing is tricky. I was not able to find a way to use FactoryBot sequences outside of the resource-construction context, so I use an Enumerator and call #next to create the sequence. This works similar to a FactoryBot sequence, except that there is no way to reset to 1 in the middle of a test run.
RSpec tests prove it works as expected, whether we are creating items in the database or building them in memory:
context 'when more than one item is created' do
let(:item_1) { create(:item) }
let(:item_2) { create(:item) }
it 'increments feature_id by 1' do
expect(item_1.feature_id).to be_present
expect(item_2.feature_id).to eq(item_1.feature_id.next)
end
end
context 'when using build instead of create' do
let(:item_1) { build(:item) }
let(:item_2) { build(:item) }
it 'increments feature_id by 1' do
expect(item_1.feature_id).to be_present
expect(item_2.feature_id).to eq(item_1.feature_id.next)
end
end
Note that you cannot create an item without a feature_id or a feature using the typical construct; for example:
>> item = create(:item, feature_id: nil)
Will result in
>> item.feature_id
#> "Feature1"
If you wish to create an object without the feature and feature_id fields, you can do:
create(:item, without_feature_id: true, without_feature: true)

Rails nested resources and devise

Let's say I have a devise model called "User" which has_many :notes and :notebooks and each :notebook has_many :notes.
So a notes will have two foreign key, :user_id and :notebook_id, so how to build/find a note?
current_user.notebooks.find(param).notes.new(params[:item]) will create the foreign_key only for notebook or also for the user in the note record in the DB?
If the second case (foreign key only for notebook), how should I write?
Using MongoDB with MongoID and referenced relations
Mongoid will manage the document references and queries for you, just make sure to specify the association/relationship for each direction that you need (e.g., User has_many :notes AND Note belongs_to :user). Like ActiveRecord, it appears to be "smart" about the relations. Please do not manipulate the references manually, but instead let your ODM (Mongoid) work for you. As you run your tests (or use the rails console), you can tail -f log/test.log (or log/development.log) to see what MongoDB operations are being done by Mongoid for you and you can see the actual object references as the documents are updated. You can see how a relationship makes use of a particular object reference, and if you pay attention to this, the link optimization should become clearer.
The following models and test work for me. Details on the setup are available on request. Hope that this helps.
Models
class User
include Mongoid::Document
field :name
has_many :notebooks
has_many :notes
end
class Note
include Mongoid::Document
field :text
belongs_to :user
belongs_to :notebook
end
class Notebook
include Mongoid::Document
belongs_to :user
has_many :notes
end
Test
require 'test_helper'
class UserTest < ActiveSupport::TestCase
def setup
User.delete_all
Note.delete_all
Notebook.delete_all
end
test "user" do
user = User.create!(name: 'Charles Dickens')
note = Note.create!(text: 'It was the best of times')
notebook = Notebook.create!(title: 'Revolutionary France')
user.notes << note
assert_equal(1, user.notes.count)
user.notebooks << notebook
assert_equal(1, user.notebooks.count)
notebook.notes << note
assert_equal(1, notebook.notes.count)
puts "user notes: " + user.notes.inspect
puts "user notebooks: " + user.notebooks.inspect
puts "user notebooks notes: " + user.notebooks.collect{|notebook|notebook.notes}.inspect
puts "note user: " + note.user.inspect
puts "note notebook: " + note.notebook.inspect
puts "notebook user: " + notebook.user.inspect
end
end
Result
Run options: --name=test_user
# Running tests:
user notes: [#<Note _id: 4fa430937f11ba65ce000002, _type: nil, text: "It was the best of times", user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), notebook_id: BSON::ObjectId('4fa430937f11ba65ce000003')>]
user notebooks: [#<Notebook _id: 4fa430937f11ba65ce000003, _type: nil, user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), title: "Revolutionary France">]
user notebooks notes: [[#<Note _id: 4fa430937f11ba65ce000002, _type: nil, text: "It was the best of times", user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), notebook_id: BSON::ObjectId('4fa430937f11ba65ce000003')>]]
note user: #<User _id: 4fa430937f11ba65ce000001, _type: nil, name: "Charles Dickens">
note notebook: #<Notebook _id: 4fa430937f11ba65ce000003, _type: nil, user_id: BSON::ObjectId('4fa430937f11ba65ce000001'), title: "Revolutionary France">
notebook user: #<User _id: 4fa430937f11ba65ce000001, _type: nil, name: "Charles Dickens">
.
Finished tests in 0.018622s, 53.6999 tests/s, 161.0998 assertions/s.
1 tests, 3 assertions, 0 failures, 0 errors, 0 skips
I would use
class User
has_many :notebooks
has_many :notes, :through => :notebooks
end
http://guides.rubyonrails.org/association_basics.html#the-has_many-through-association
Update
You could always set the user_id manually, like this (I assume param is the ID for your notebook?):
Notebook.find(param).notes.new(params[:item].merge(:user_id => current_user.id))

FactoryGirl: attributes_for not giving me associated attributes

I have a Code model factory like this:
Factory.define :code do |f|
f.value "code"
f.association :code_type
f.association(:codeable, :factory => :portfolio)
end
But when I test my controller with a simple test_should_create_code like this:
test "should create code" do
assert_difference('Code.count') do
post :create, :code => Factory.attributes_for(:code)
end
assert_redirected_to code_path(assigns(:code))
end
... the test fails. The new record is not created.
In the console, it seems that attributes_for does not return all required attributes like the create does.
rob#compy:~/dev/my_rails_app$ rails console test
Loading test environment (Rails 3.0.3)
irb(main):001:0> Factory.create(:code)
=> #<Code id: 1, code_type_id: 1, value: "code", codeable_id: 1, codeable_type: "Portfolio", created_at: "2011-02-24 10:42:20", updated_at: "2011-02-24 10:42:20">
irb(main):002:0> Factory.attributes_for(:code)
=> {:value=>"code"}
Any ideas?
Thanks,
You can try something like this:
(Factory.build :code).attributes.symbolize_keys
Check this: http://groups.google.com/group/factory_girl/browse_thread/thread/a95071d66d97987e)
This one doesn't return timestamps etc., only attributes that are accessible for mass assignment:
(FactoryGirl.build :position).attributes.symbolize_keys.reject { |key, value| !Position.attr_accessible[:default].collect { |attribute| attribute.to_sym }.include?(key) }
Still, it's quite ugly. I think FactoryGirl should provide something like this out of the box.
I opened a request for this here.
I'd suggest yet an other approach, which I think is clearer:
attr = attributes_for(:code).merge(code_type: create(:code_type))
heres what I end up doing...
conf = FactoryGirl.build(:conference)
post :create, {:conference => conf.attributes.slice(*conf.class.accessible_attributes) }
I've synthesized what others have said, in case it helps anyone else. To be consistent with the version of FactoryGirl in question, I've used Factory.build() instead of FactoryGirl.build(). Update as necessary.
def build_attributes_for(*args)
build_object = Factory.build(*args)
build_object.attributes.slice(*build_object.class.accessible_attributes).symbolize_keys
end
Simply call this method in place of Factory.attributes_for:
post :create, :code => build_attributes_for(:code)
The full gist (within a helper module) is here: https://gist.github.com/jlberglund/5207078
In my APP/spec/controllers/pages_controllers_spec.rb I set:
let(:valid_attributes) { FactoryGirl.attributes_for(:page).merge(subject: FactoryGirl.create(:theme), user: FactoryGirl.create(:user)) }
Because I have two models associated. This works too:
FactoryGirl.define do
factory :page do
title { Faker::Lorem.characters 12 }
body { Faker::Lorem.characters 38 }
discution false
published true
tags "linux, education, elearning"
section { FactoryGirl.create(:section) }
user { FactoryGirl.create(:user) }
end
end
Here's another way. You probably want to omit the id, created_at and updated_at attributes.
FactoryGirl.build(:car).attributes.except('id', 'created_at', 'updated_at').symbolize_keys
Limitations:
It does not generate attributes for HMT and HABTM associations (as these associations are stored in a join table, not an actual attribute).
Association strategy in the factory must be create, as in association :user, strategy: :create. This strategy can make your factory very slow if you don't use it wisely.

Problem on Rails fixtures creation sequence

I am reading the book Simply Rails by Sitepoint and given these models:
story.rb
class Story < ActiveRecord::Base
validates_presence_of :name, :link
has_many :votes do
def latest
find :all, :order => 'id DESC', :limit => 3
end
end
def to_param
"#{id}-#{name.gsub(/\W/, '-').downcase}"
end
end
vote.rb
class Vote < ActiveRecord::Base
belongs_to :story
end
and given this fixtures
stories.yml
one:
name: MyString
link: MyString
two:
name: MyString2
link: MyString2
votes.yml
one:
story: one
two:
story: one
these tests fail:
story_test.rb
def test_should_have_a_votes_association
assert_equal [votes(:one),votes(:two)], stories(:one).votes
end
def test_should_return_highest_vote_id_first
assert_equal votes(:two), stories(:one).votes.latest.first
end
however, if I reverse the order of the stories, for the first assertion and provide the first vote for the first assertion, it passes
story_test.rb
def test_should_have_a_votes_association
assert_equal [votes(:two),votes(:one)], stories(:one).votes
end
def test_should_return_highest_vote_id_first
assert_equal votes(:one), stories(:one).votes.latest.first
end
I copied everything as it is in the book and have not seen an errata about this. My first conclusion was that the fixture is creating the records from bottom to top as it was declared, but that doesn't make any point
any ideas?
EDIT: I am using Rails 2.9 running in an RVM
Your fixtures aren't getting IDs 1, 2, 3, etc. like you'd expect - when you add fixtures, they get IDs based (I think) on a hash of the table name and the fixture name. To us humans, they just look like random numbers.
Rails does this so you can refer to other fixtures by name easily. For example, the fixtures
#parents.yml
vladimir:
name: Vladimir Ilyich Lenin
#children.yml
joseph:
name: Joseph Vissarionovich Stalin
parent: vladimir
actually show up in your database like
#parents.yml
vladimir:
id: <%= fixture_hash('parents', 'vladimir') %>
name: Vladimir Ilyich Lenin
#children.yml
joseph:
id: <%= fixture_hash('children', 'joseph') %>
name: Joseph Vissarionovich Stalin
parent_id: <%= fixture_hash('parents', 'vladimir') %>
Note in particular the expansion from parent: vladimir to parent_id: <%= ... %> in the child model - this is how Rails handles relations between fixtures.
Moral of the story: Don't count on your fixtures being in any particular order, and don't count on :order => :id giving you meaningful results in tests. Use results.member? objX repeatedly instead of results == [obj1, obj2, ...]. And if you need fixed IDs, hard-code them in yourself.
Hope this helps!
PS: Lenin and Stalin weren't actually related.
Xavier Holt already gave the main answer, but wanted to also mention that it is possible to force rails to read in fixtures in a certain order.
By default rails assigns its own IDs, but you can leverage the YAML omap specification to specify an ordered mapping
# countries.yml
--- !omap
- netherlands:
id: 1
title: Kingdom of Netherlands
- canada:
id: 2
title: Canada
Since you are forcing the order, you have to also specify the ID yourself manually, as shown above.
Also I'm not sure about this part, but I think once you commit to overriding the default rails generated ID and use your own, you have to do the same for all downstream references.
In the above example, suppose each country can have multiple leaders, you would have do something like
# leaders.yml
netherlands-leader:
country_id: 1 #you have to specify this now!
name: Willem-Alexander
You need to manually specify the id that refers to the previous Model (Countries)

Resources