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!
Related
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.
I am trying to get a date from a user and send it inside an email as plain text in the following format: "07/30/2015".
In order to do that, if the output I am getting is a string, I could just do:
Date.parse("2015-07-30").strftime("%m/%d/%Y")
The problem is, I am getting a FixNum.
The issues are many:
If I try to convert to a string to parse it with Date.parse, it becomes "2001".
If I apply the code I just wrote, Date.parse... it will throw 'invalid date'.
For instance:
(2016-02-13).to_s #=> "2001"
(2016-02-13).to_date #=> NoMethodError: undefined method `to_date' for 2001:Fixnum
Date.parse("2001").strftime("%m/%d/%Y") #=> invalid date
So if I can convert 2015-07-30 into "2015-07-30", it would work:
Date.parse("2015-07-30").strftime("%m/%d/%Y") #=> "07/30/2015"
Then I tried using date_select instead of date_field, but now the message arrives with those fields empty.
Any suggestions?
Here is my form:
= form_for #contact do |f|
= f.text_field :product_name
= f.date_field :purchase_date
= f.submit
Here is my code:
<%= message.subject %>
<% #resource.mail_form_attributes.each do |attribute, value|
if attribute == "mail_subject"
next
end
%>
<%= "#{#resource.class.human_attribute_name(attribute)}: #{Date.parse(value).class == Date ? Date.parse(value).strftime("%m/%d/%Y") : value}" %>
<% end %>
My controller:
class ContactsController < ApplicationController
before_action :send_email, except: [:create]
def create
#contact = Contact.new(params[:contact])
#contact.request = request
if #contact.deliver
#thank = "Thank you for your message!"
#message = "We have received your inquiry and we'll be in touch shortly."
else
#error = "Cannot send message. Please, try again."
end
end
def contact_page
end
def product_complaint
#the_subject = "Product Complaint Form"
end
private
def send_email
#contact = Contact.new
end
end
My model:
class Contact < MailForm::Base
# all forms
attribute :mail_subject
attribute :first_name, validate: true
attribute :last_name, validate: true
# product-complaint
attribute :best_by, validate: true, allow_blank: true # date
attribute :bag_code, validate: true, allow_blank: true
attribute :purchase_date, validate: true, allow_blank: true # date
attribute :bag_opened, validate: true, allow_blank: true # date
attribute :problem_noticed, validate: true, allow_blank: true # date
# all forms
attribute :message, validate: true
attribute :nickname, captcha: true
def headers
{
content_type: "text/plain",
subject: %(#{mail_subject}),
to: "xxxxx#xxxxxxx.com",
# from: %("#{first_name.capitalize} #{last_name.capitalize}" <#{email.downcase}>)
from: "xxx#xxxxx.com"
}
end
end
(2016-02-13).to_date #=> NoMethodError: undefined method `to_date' for 2001:Fixnum
youre getting this error because you dont have quotes around the value. i.e. its not a string, its a number that is having subtraction applied to it. this is being interpreted as
2016 - 2
2014 - 13
2001.to_date
it needs to be ('2016-02-13').to_date
if youre unable to get it as a string, can you post how you're getting it from the user to begin with? (a date field ought to be sending you a string to your controller, not a series of numbers)
You're not understanding something about receiving values from forms: You can NOT receive an integer, a fixnum or anything else other than strings. So, you can't have received 2016-02-13. Instead you got "2016-02-13" or "2016", "02" or "2" and "13" depending on the form. If you're running under Rails, then it got the strings, and through its meta-data understands you want an integer (which really should probably be defined as a string), and it converts it to an integer for you.
Either way, when you write:
(2016-02-13).to_s
(2016-02-13).to_date
you're propagating that misunderstanding into your testing. This is how it MUST be written because you need to be working with strings:
require 'active_support/core_ext/string/conversions'
("2016-02-13").to_s # => "2016-02-13"
("2016-02-13").to_date # => #<Date: 2016-02-13 ((2457432j,0s,0n),+0s,2299161j)>
You can create dates without them being strings though: Ruby's Date initializer allows us to pass the year, month and day value and receive a new Date object:
year, month, day = 2001, 1, 2
date = Date.new(year, month, day) # => #<Date: 2001-01-02 ((2451912j,0s,0n),+0s,2299161j)>
date.year # => 2001
date.month # => 1
date.day # => 2
Moving on...
Parsing dates in Ruby quickly demonstrates it's not a U.S.-centric language. Americans suppose all dates of 01/01/2001 are in "MM/DD/YYYY" but that's a poor assumption because much of the rest of the world uses "DD/MM/YYYY". Not knowing that means that code written under that assumption is doing the wrong thing. Consider this:
require 'date'
date = Date.parse('01/02/2001')
date.month # => 2
date.day # => 1
Obviously something "wrong" is happening, at least for 'mericans. This is very apparent with:
date = Date.parse('01/31/2001')
# ~> -:3:in `parse': invalid date (ArgumentError)
This occurs because there is no month "31". In the previous example of '01/02/2001', that misunderstanding means the programmer thinks it should be "January 2" but the code thinks it's "February 1", and work with that. That can cause major havoc in an enterprise system, or anything dealing with financial calculations, product scheduling, shipping or anything else that works with dates.
Because the code is assuming DD/MM/YYYY format for that sort of string, the sensible things to do are:
KNOW what format your users are going to send dates in. Don't assume, ever. ASK them and make your code capable of dealing with alternates, or tell them what they MUST use and vet out their data prior to actually committing it to your system. Or, provide a GUI that forces them to pick their dates from popups and never allows them to enter it by hand.
Force the date parser to use explicit formats of dates so it can always do the right thing:
Date.strptime('01/31/2001', '%m/%d/%Y') # => #<Date: 2001-01-31 ((2451941j,0s,0n),+0s,2299161j)>
Date.strptime('31/01/2001', '%d/%m/%Y') # => #<Date: 2001-01-31 ((2451941j,0s,0n),+0s,2299161j)>
The last point is the crux of writing code: We're telling the language what to do, not subjecting ourselves, and our employers, to code that's guessing. Give code half a chance and it'll do the wrong thing, so you control it. That's why programming is hard.
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!
So, here's my custom rake task:
task :backup => :environment do |t|
SeedFu::Writer.write('/path/to/file.rb', class_name: 'Category', constraints: [:id] do |w|
Category.all.each do |x|
w << x
end
end
end
And the following result file contains:
# DO NOT MODIFY THIS FILE, it was auto-generated.
#
# Date: 2014-06-15 21:08:13 +0700
# Seeding Category
# Written with the command:
#
# /home/dave/.rvm/gems/ruby-2.1.2/bin/rake backup
#
Category.seed(:id,
#<Category id: 1, name: "foo">,
#<Category id: 2, name: "bar">,
#<Category id: 3, name: "baz">
)
# End auto-generated file.
Question: Why did the seedfile got commented out?
Thanks!
So, this is a basic string manipulation.
When I read closely on their source code, seed method accepts Hash, not object.
So, I simply translated the object to its Hash equivalent:
task :backup => :environment do |t|
SeedFu::Writer.write('/path/to/file.rb', class_name: 'Category', constraints: [:id] do |w|
Category.all.each do |x|
w << x.as_json
end
end
end
Note that you can use .attributes or .as_json, but I read somewhere that .attributes actually takes a lot more time than .as_json.
After that, I encountered yet another problem: Datetime columns. When converted to JSON, Datetime columns are not quoted. So what I did was:
Store the column names (of type Datetime) to an array.
Store current object's Hash to a local variable.
Convert the Datetime values to String, using .to_s (in the local variable)
Output the modified local variable to writer object.
Hope this helps.
Experiencing exact the same problems, commented output and datetime columns are not quoted. It seems that ActiveSupport::JSON could kill two birds with one stone.
require 'seed-fu'
j = ActiveSupport::JSON
SeedFu::Writer.write("#{Rails.root}/db/dump_seeds/lectures.rb",{ class_name: 'Lecture', constraints: [:id, :name]}) do |writer|
Lecture.all.order('id').each do |e|
writer << j.decode(j.encode(e))
end
end
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)