问题
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.
回答1:
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 theupper_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.
回答2:
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!
来源:https://stackoverflow.com/questions/31299163/has-many-through-creating-child-after-save-actionviewtemplateerror