Problem on Rails fixtures creation sequence - ruby-on-rails

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)

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 to setup fixtures when using Mobility with a Key Value backend?

Got a question on how to setup fixtures for Mobility. Would be very grateful for any tips on how to get this going and would be a valuable lesson for me as well on how to tackle setting up fixtures in general.
Not using any gems to setup fixtures, just the default Rails approach for this case. I have a Song model which has multiple translatable attributes, title uses Mobility, description and content use Mobility Action Text.
It works really well but when setting up fixtures I'm finding it difficult to relate the records. There's three tables at play here songs where the only field used is status. mobility_string_translations stores translations for title and action_text_rich_texts stores translated descriptions and content.
This is how my translation setup looks like in Song:
class Song < ApplicationRecord
extend Mobility
validates :title_pt, presence: true
validates :status, inclusion: { in: %w(draft published private) },
presence: true
translates :title, type: :string, locale_accessors: I18n.available_locales
translates :description, backend: :action_text, locale_accessors: I18n.available_locales
translates :content, backend: :action_text, locale_accessors: I18n.available_locales
# file continuation...
As for fixtures songs.yml looks like this:
one:
status: "published"
Then based on what I've found online I've created mobility/string_translations.yml with the following content:
one:
translatable_id: one (Song)
translatable_type: "Song"
key: "title"
value: "Title in English"
locale: "en"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
two:
translatable_id: one (Song)
translatable_type: "Song"
key: "title"
value: "Titulo em Português"
locale: "pt"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
This seems to work but I know it isn't because when I inspect #song = songs(:one) looking for translated values (#song.title_pt and #song.title_en) they're both nil.
Any idea on what to do here? 🙏
I think the issue is with how you declared the translatable relationship in the mobility_string_translation table.
It should be either the fully explicit form.
translatable_id: 1
translatable_type: Song
Or the shorthand provided by fixtures.
translatable: one (Song)
Here you are kind of mixing both.
It's documented here in the Polymorphic belongs_to https://api.rubyonrails.org/classes/ActiveRecord/FixtureSet.html.
I've managed to make the explicit form work but not the shorthand for some reason.
I've been looking for instructions on how to make fixtures work with mobility myself and you provided the heavy lifting with your example so thanks a lot for that.
The issue in my case is that translatable_type was Song instead of "Song" and it couldn't map the records in mobility_string_translations to the correct Song record. Here's a bit more detail on the setup that I have that does work to write tests:
By work I mean, the Mobility translation records defined in fixture files are detected and can be used to compose tests. Running #song.title_en should output a value instead of nil.
Let's consider the following Song model, it has a title that can be translated and a status which is only used to affect the visibility of the song in the front end. Fixtures for a couple of Songs would look like this:
# test/fixtures/songs.yml
one:
id: 1
status: "published"
two:
id: 2
status: "draft"
The id is usually not specified in fixtures but here it becomes necessary so that we're sure which identifier to use when pointing translated records.
The Mobility implementation will store any translated titles at mobility_string_translations the following can be added to test/fixtures/mobility/string_translations.yml:
# test/fixtures/mobility/string_translations.yml
song_one_en:
translatable_id: 1
translatable_type: "Song"
key: "title"
value: "Maçaranduba Wood"
locale: "en"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
song_one_pt:
translatable_id: 1
translatable_type: "Song"
key: "title"
value: "Madeira de Maçaranduba"
locale: "pt"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
song_two_en:
translatable_id: 2
translatable_type: "Song"
key: "title"
value: "Dona Maria from Camboatá"
locale: "en"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
song_two_pt:
translatable_id: 2
translatable_type: "Song"
key: "title"
value: "Dona Maria do Camboatá"
locale: "pt"
created_at: <%= Time.now %>
updated_at: <%= Time.now %>
Each song includes a title for English and Portuguese in this case but any locales the record is going to make use of or need to be tested can be included here, in an individual record.
The important aspect here is that all translatable_type columns are explicit string types.
For example, do "Song", instead of Song when adding a value to the property.
Setting up fixtures with this method associates translated properties to a record and enables them to be accessed in a test.
For example, to change the title of a song, the record can be brought into the test in a setup block and the title translations will be available and can be modified:
# test/controllers/song_controller_test.rb
require "test_helper"
class SongControllerTest < ActionDispatch::IntegrationTest
setup do
#song = songs(:one)
end
test "admin can edit a song" do
# Keeps a copy of the original record for comparison.
current_record = #song
# Passes the locale to the request helper to keep it from getting confused with the record id.
# Changes the title of the record.
patch song_url(I18n.locale, #song, { song: { title_en: 'Updated Song Title' } })
# Retrieves the same record to be used for comparison.
updated_record = Song.find(#song.id)
# Checks that a change actually occurred.
assert current_record.updated_at != updated_record.updated_at
# Checks that the list of songs is being displayed to the user.
assert_redirected_to songs_path
end
end
To make sure that the fixture has setup the association bettween the model and the translated records, the debugger method can be used. Start by adding it as a breakpoint to your test logic, in this case I'm going to use the example above:
# test/controllers/song_controller_test.rb
test "admin can edit a song" do
current_record = #song
patch song_url(I18n.locale, #song, { song: { title_en: 'Updated Song Title' } })
updated_record = Song.find(#song.id)
debugger # <-- The script will pause here.
assert current_record.updated_at != updated_record.updated_at
assert_redirected_to songs_path
end
Then the test can be run, bin/rails test would work but in this example the command to run just the tests for this file would be:
bin/rails test test/controllers/role_controller_test.rb
The output in the terminal will look similar to this, the program will be paused at this point and it is interactive:
bin/rails test test/controllers/song_controller_test.rb
Running 26 tests in a single process (parallelization threshold is 50)
Run options: --seed 56548
# Running:
.............[64, 73] in ~/Projects/rails_app/test/controllers/song_controller_test.rb
64|
65| current_record = #song
66| patch song_url(I18n.locale, #song, { song: { title_en: 'Updated Song Title' } })
67| updated_record = Song.find(#song.id)
68|
=> 69| debugger # <-- The script will pause here.
70|
71| # Checks that a change actually occurred.
72| assert current_record.updated_at != updated_record.updated_at
73|
=>
#0 block in <class:SongControllerTest> at ~/Projects/rails_app/test/controllers/song_controller_test.rb:69
#1 block in run (3 levels) at ~/.rbenv/versions/3.1.0/lib/ruby/gems/3.1.0/gems/minitest-5.15.0/lib/minitest/test.rb:98
# and 24 frames (use `bt' command for all frames)
(rdbg)
Any variables defined before debugger can be accessed, this can be used to inspect if #song was changed:
(rdbg) #song.title_en # ruby
"Updated Song Title"
(rdbg) #song.title_pt # ruby
"Madeira de Maçaranduba"
The title was updated using the patch request defined in the test case. Typing continue will move on from the breakpoint and continue running the code in the test file.
That should be it!

How to pluck multiple attributes in Rails 3.x?

I have an ActiveRecord model like this:
class Person < ActiveRecord::Base
attr_accessible :name
end
and need to get a hash mapping Person's ids to their names:
{1 => "Paul", 2 => "Aliyah", 3 => ... }
Now, the obvious way would be
Person.all.collect { |p| [p.id, p.name] }.to_h
However, I don't need to instantiate every Person, I just need the hash. In Rails 4, I can .pluck(:id, :name) instead of collect, however in 3.x, pluck takes only one argument. However I found this workaround to get what I want without loading the models:
Person.all.group(:id).minimum(:name)
Question: will I burn in hell? Also, is there a more elegant way to do this, and are there any drawbacks of this hacky approach that I may not be aware of? Thanks!
Here's a pretty good write up of this situation and various tactics for handling it: Plucking Multiple Columns in Rails 3
My preference of suggested solutions there is to make and include a module:
# multi_pluck.rb
require 'active_support/concern'
module MultiPluck
extend ActiveSupport::Concern
included do
def self.pluck_all(relation, *args)
connection.select_all(relation.select(args))
end
end
end
class Person < ActiveRecord::Base
attr_accessible :name
def self.pluck_id_and_name
result = connection.select_all(select(:id, :name))
if result.any?
# if you are using Ruby 2.1+
result.to_h
# Works in 1.9.3+
Hash[result]
end
end
end
Since the result should be an array of arrays we can use nifty trick to get a hash with the first element as keys and the second as values:
Hash[ [ [1, "Joe"], [2, "Jill"] ] ]
# => { 1 => "Joe", 2 => "Jill"}
See:
Convert array of 2-element arrays into a hash, where duplicate keys append additional values
To avoid loading all of the objects you could do this:
hash = Hash.new
ActiveRecord::Base.connection.execute("SELECT id, name FROM persons").each {|person| hash[person['id'].to_s] = person['name'].to_s}
My company used one of the tactics from Plucking Multiple Columns in Rails 3 before.
But we had trouble upgrading from Rails 3 to Rails 4 because it didn't work in Rails 4.
I suggest using pluck_all gem which has high test coverage in Rails 3, 4, 5, so you will not worry about future upgrades.

has_many :through creating child after_save --> ActionView::Template::Error

I have three models: List, Food, and Quantity. List and Food are associated through Quantity via has_many :through. The model association is doing what I want, but when I test, there is an error.
test_valid_list_creation_information#ListsCreateTest (1434538267.92s)
ActionView::Template::Error: ActionView::Template::Error: Couldn't find Food with 'id'=14
app/views/lists/show.html.erb:11:in `block in _app_views_lists_show_html_erb__3286583530286700438_40342200'
app/views/lists/show.html.erb:10:in `_app_views_lists_show_html_erb__3286583530286700438_40342200'
test/integration/lists_create_test.rb:17:in `block (2 levels) in <class:ListsCreateTest>'
test/integration/lists_create_test.rb:16:in `block in <class:ListsCreateTest>'
app/views/lists/show.html.erb:11:in `block in _app_views_lists_show_html_erb__3286583530286700438_40342200'
app/views/lists/show.html.erb:10:in `_app_views_lists_show_html_erb__3286583530286700438_40342200'
test/integration/lists_create_test.rb:17:in `block (2 levels) in <class:ListsCreateTest>'
test/integration/lists_create_test.rb:16:in `block in <class:ListsCreateTest>'
My aim is to create a new Quantity (associated with that list) each time a list is created. Each Quantity has amount, food_id, and list_id.
list_id should equal the id of the list that was just created.
food_id should equal the id of a random food that already exists.
amount should be a random integer.
In the error, the number 14 ("Food with 'id'=14) is generated by randomly selecting a number from 1 to Food.count. Food.count equals the number of food objects in test/fixtures/foods.yml, so the foods are definitely recognized, at least when I run Food.count. So why wouldn't food with 'id'=14 exist?
I believe there is something wrong with either the Lists controller, the fixtures, or the integration test. Whatever is causing the test to fail doesn't seem to affect performance (everything works in the console and server/user interface), but I am trying to understand TDD and write good tests, so I will appreciate any guidance.
Lists model:
class List < ActiveRecord::Base
has_many :quantities
has_many :foods, :through => :quantities
validates :days, presence: true
validates :name, uniqueness: { case_sensitive: false }
after_save do
Quantity.create(food_id: rand(Food.count), list_id: self.id, amount: rand(6))
end
end
Quantities fixture:
one:
food: grape
list: weekend
amount: 1
two:
food: banana
list: weekend
amount: 1
Note: the Quantities fixture was previously organized as follows ...
one:
food_id: 1
list_id: 1
amount: 1
... and it seems to make no difference.
lists_create integration test:
require 'test_helper'
class ListsCreateTest < ActionDispatch::IntegrationTest
test "invalid list creation information" do
get addlist_path
assert_no_difference 'List.count' do
post lists_path, list: { days: "a",
name: "a" * 141 }
end
assert_template 'lists/new'
end
test "valid list creation information" do
get addlist_path
assert_difference 'List.count', 1 do
post_via_redirect lists_path, list: {
days: 2,
name: "example list"
}
end
assert_template 'lists/show'
end
end
And app/views/lists/show.html.erb referenced in the error:
<% provide(:title, #list.name) %>
<div class="row"><aside class="col-md-4"><section class="user_info">
<h1> <%= #list.name %></h1>
<p><%= #list.days %> day(s)</p><p>
<% Quantity.where(:list_id => #list.id).each do |f| %>
<%= "#{f.amount} #{Food.find(f.food_id).name}" %>
<% end %>
</p></section></aside></div><%= link_to "edit the properties of this list", edit_list_path %>
Thank you for any advice or references. Please let me know if you need other code or information that you consider relevant. I am hoping to accomplish this all using fixtures and not another method such as FactoryGirl, even if it means a little extra code.
Rails 4.2.3, Cloud9. Development database = SQLite3, production database = postgres heroku.
Besides being very weird to create a random value in the after_save callback (which I think you're doing as an exercise, but anyway it's better to use good practices from the start), you should never use rand(Model.count) to get a sample record. There's two main problems:
The rand(upper_bound) method returns a number between zero and the upper_bound argument, but there's no guarantee that zero is the first created id. I'm using PostgreSQL and the first model has the id 1. You can specify a range (rand(1..upper_bound)), but anyway you're gambling on the way the current database works.
You're assuming that all the records exist in a sequential order at any given time, which is not always true. If you delete a record and it's id is randomly chosen, you'll get an error. The library also can use any strategy to create the fixtures, so it's better not to assume anything about how it works.
If you really need to choose randomly a record, I'd recommend simply using the array's sample method: Food.all.sample. It's slow, but it works. If you need to optimize, there's other options.
Now, I'd really recommend to avoid random values at all costs, using them only when necessary. It's difficult to test, and difficult to track bugs. Also, I'd avoid creating a relation inside a callback, it grows rapidly into a unmanageable mess.
I am posting an answer because after implementing the suggestions, my error is gone and I think I have a better understanding of what's going on.
Previously, I had Quantities created in the List model upon creation of a List using a relation. The relation is now in the controller, not the model.
List model without relation:
class List < ActiveRecord::Base
has_many :quantities
has_many :foods, :through => :quantities
validates :days, presence: true
validates :name, uniqueness: { case_sensitive: false }
end
Quantities fixture and lists_create integration test are unchanged.
Previously this show.html.erb contained a query. Now, it has only #quantities, which is defined in the Lists controller. The query is in the controller, not the view.
app/views/lists/show.html.erb:
<% provide(:title, #list.name) %>
<div class="row"><aside class="col-md-4"><section class="user_info">
<h1> <%= #list.name %></h1>
<p><%= #list.days %> day(s)</p>
<p><%= #quantities %></p>
</section></aside></div><%= link_to "edit the properties of this list", edit_list_path %>
The List controller with the query in the show method (to filter for quantities that have the proper list_id) and the relation in the create method (to create new quantities upon list creation).
class ListsController < ApplicationController
def show
#list = List.find(params[:id])
#quantities = []
Quantity.where(:list_id => #list.id).each do |f|
#quantities.push("#{f.amount} #{Food.find(f.food_id).name}")
end
end
# ...
def create
#list = List.new(list_params)
if #list.save
flash[:success] = "A list has been created!"
#a = Food.all.sample.id
#b = Food.all.sample.id
Quantity.create(food_id: #a, list_id: #list.id, amount: rand(6))
if (#a != #b)
Quantity.create(food_id: #b, list_id: #list.id, amount: rand(6))
end
redirect_to #list
else
render 'new'
end
end
# ...
end
If I understand correctly, I was misusing the model and view and inappropriately using rand with Food.count.
Please let me know if you think I've missed anything or if you can recommend anything to improve my code. Thank you #mrodrigues, #jonathan, and #vamsi for your help!

How to make has_many :through association with fixtures?

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]}

Resources