I would like to create complex rest object instances with a single rest call using rails.
In the example case below I get an error in the controller when I call new on Person with a parameter hash.
I get an error for unexpected type when seeing a ActiveSupport::HashWithIndifferentAccess and not a PhoneNumber
The hash passed from the test contains an array of Hash objects, while the controller action parameters create ActiveSupport::HashWithIndifferentAccess objects.
Any suggestions to fix the error?
Is there an easier way to create complex activerecord objects with a single rest call.
ie models:
class Person < ActiveRecord::Base
has_many :phone_numbers , :autosave => true
class PhoneNumber < ActiveRecord::Base
belongs_to :person
person_controller_test.rb
test "should create person" do
newperson=Person.new(:name => "test")
newperson.phone_numbers << PhoneNumber.new(:number => "123-4567")
person_string= newperson.to_xml(:include => :phone_numbers)
person_hash=Hash.from_xml(course_string)
person_hash2=person_hash['person']
post :create, :person => person_hash2, :format => "xml"
assert_response :success
end
person_controller.rb
def create
#person = Person.new(params[:person])
class Person < ActiveRecord::Base
has_many :phone_numbers , :autosave => true
# this is important for create complex nested object in one call
accepts_nested_attributes_for :phone_numbers
end
class PhoneNumber < ActiveRecord::Base
belongs_to :person
end
person_controller_test.rb
test "should create person" do
newperson=Person.new(:name => "test")
newperson.phone_numbers.build(:number => "123-4567") #more cleaner
# and start from here I'm not sure but this maybe help you
# I think that you must pass a json object
post :create, :person => newperson.to_json(:include => :phone_numbers), :format => "xml"
assert_response :success
end
link: http://api.rubyonrails.org/classes/ActiveRecord/NestedAttributes/ClassMethods.html
Dinatih, Thanks for the helpful answer! It helped solve the issue.
I ran into a slight problem since with "accepts_nested_attributes_for :phone_numbers",
the hash key 'phone_numbers_attributes' is needed instead of the to_xml/to_json serialization default of 'phone_numbers'. The test code (below) looks a little ugly, but it passes and creates the object correctly. Also passing json to the post method unfortunately doesn't create the object.
test "should create complex person" do
newperson=Person.new(:name => "test")
newperson.phone_numbers.build(:number => "123-4567")
person_string= newperson.to_xml(:include => :phone_numbers)
person_hash=Hash.from_xml(person_string)
person_hash2=person_hash['person']
person_hash2[:phone_numbers_attributes] = person_hash2['phone_numbers']
person_hash2.delete('phone_numbers')
p person_hash2
post :create, :person => person_hash2, :format => "xml"
p response.body
assert_select "person" do
assert_select "name", {:text=>"test"}
assert_select "phone-numbers" do
assert_select "phone-number" do
assert_select "number", {:text=>"123-4567"}
end
end
end
assert_response :success
end
you should also check out:
Gem nested_form :
https://github.com/ryanb/nested_form
examples for nested_form: https://github.com/ryanb/complex-form-examples/tree/nested_form
and
RailsCasts 196 / 197
http://railscasts.com/episodes/196-nested-model-form-part-1
http://railscasts.com/episodes/197-nested-model-form-part-2
Related
I'm writing some tests for my Rails project. I've come to writing some tests around a polymorphic association, for which I have defined a fixture. However, when I try to access the polymorphically associated field on that fixture, it returns nil.
The strange part is that while the field itself is nil, the _id and _type fields that describe it are not.
This is the model:
class Comment < ActiveRecord::Base
default_scope { where(is_deleted: false) }
belongs_to :post, :polymorphic => true
belongs_to :user
end
(In case it wasn't obvious, the post association is the polymorphic.)
This is the action in the controller that I'm trying to test:
class CommentsController < ApplicationController
# ...
def update
if #comment.update comment_params
puts #comment.post.nil?
puts #comment.post_id.nil?
puts #comment.post_type.nil?
if #comment.post_type == 'Question'
redirect_to url_for(:controller => :questions, :action => :show, :id => #comment.post.id)
else
redirect_to url_for(:controller => :questions, :action => :show, :id => #comment.post.question.id)
end
else
flash[:error] = "Comment failed to update."
if #comment.post_type == 'Question'
redirect_to url_for(:controller => :questions, :action => :show, :id => #comment.post.id)
else
redirect_to url_for(:controller => :questions, :action => :show, :id => #comment.post.question.id)
end
end
end
private
def comment_params
params.require(:comment).permit(:content, :post_type, :post_id)
end
# ...
end
The test itself looks like this:
test "should update existing comment" do
sign_in users(:standard_user)
patch :update, :id => comments(:one).id, :comment => { :content => "ABCDEF GHIJKL MNOPQR STUVWX YZ" }
assert_not_nil assigns(:comment)
assert_not_nil assigns(:comment).post
assert_response(302)
end
And, finally, the fixture for that existing comment (comments(:one)) is:
one:
user: standard_user
post: one (Answer)
content: ABCDEF GHIJKL MNOPQR
Both the standard_user User fixture and the one Answer fixture are correctly defined.
Running the test gives me this error:
2) Error:
CommentsControllerTest#test_should_update_existing_comment:
NoMethodError: undefined method `question' for nil:NilClass
app/controllers/comments_controller.rb:50:in `update'
test/controllers/comments_controller_test.rb:16:in `block in <class:CommentsControllerTest>'
The three log lines near the top of the update action output true \n false \n false - i.e. that the post field is nil, but that post_id and post_type are not.
Why is this? What can I do to fix it? I have no ID fields defined in my fixtures YAML, and I'd prefer not to have to specify the IDs (unless it can be done with ERB).
This is the entirety of the Answers fixture that the Comments depend on:
one:
body: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ
score: 0
question: one
user: standard_user
two:
body: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ
score: 0
question: one
user: editor
That in turn depends on this Question fixture:
one:
title: ABCDEF GHIJKL MNOPQR STUVWX YZ
body: ABCDEF GHIJKL MNOPQR STUVWX YZ ABCDEF GHIJKL MNOPQR STUVWX YZ
tags:
- ABCDEF
- GHIJKL
- MNOPQR
score: 0
user: standard_user
It seems that this issue is caused by there being no Answer fixtures in the database when the comments tests happen (so the call to #comment.post returns nil, and #comment.post.question or #comment.post.id raises a NoMethodError). However, the test helper file calls fixtures :all, so I don't see why those fixtures aren't loaded.
This happens when ActiveRecord couldn't find the polymorphic association.
Even though the post_id and post_type have values, but Answer with id by comment.post_id could not be found, it will just return nil when you call comment.post.
Could you paste your answers.yml fixture? I think you might have put an id in it accidentally.
EDIT:
I found the bug in your project. You didn't add default value for is_deleted in answers and comments tables, so you won't find the correct record since their is_deleted is NULL actually. My advice would be: never use default_scope, you can always have another better solution than that.
Trying to get this function test to pass:
test "should create question" do
assert_difference('Question.count') do
post :create, :question => #question.attributes
end
end
But #question has validators that require specific children to be present specifically one topic:
class Question < ActiveRecord::Base
has_many :topic_questions
has_many :topics, :through => :topic_questions
validate :has_topic
def has_topic
(errors[:base] << "You must have one topic") if (topics.count < 1)
end
end
How would I 1) build the topic for #question in the test and then 2) pass it to the post method since it wouldnt be passed by the .attributes() function?
test "should create question" do
assert_difference('Question.count') do
#question.topics<<Topic.new(**set topics required and attribute here )
#or try this line of code
#question[:topics]={:name=>"bla bla" ** set attribute here what u need}
post :create, :question => #question.attributes
end
end
The test is fine, it's the controller and/or model that needs changing. You haven't shown the contents of the create action, but there are basically two ways to do it:
#question = Question.new(params[:question])
#question.build_topic(<some_params>)
if #question.save
# ... etc ...
Or, use accepts_nested_attributes_for :topic in the Question model and then pass the topic parameters in the params hash. Which method is best depends on your specific circumstances.
I have an accounts model that holds some basic account info (account name, website, etc). I then have a user model that has the following in the app/models/user.rb
belongs_to :account
I also have the following in my routes.rb
map.resources :account, :has_many => [:users, :othermodel]
the problem I'm facing is that the following test is failing:
test "should create user" do
assert_difference('User.count') do
post :create, :user => { } #this is the line it's actually failing on
end
assert_redirected_to user_path(assigns(:user)) #it doesn't get here yet
end
The error it gives is "Can't find Account without ID" so I kind of understand WHY it's failing, because of the fact that it doesn't have the account object (or account_id as it were) to know under what account to create the user. I have tried variations of the following but I am completely lost:
post :create, :user => { accounts(:one) } #I have the 'fixtures :accounts' syntax at the top of the test class
post :create, [accounts(:one), :user] => { }
post :create, :user => { accounts(:one), #other params for :user }
and like I said, just about every variation I could think of. I can't find much documentation on doing this and this might be why people have moved to Factories for doing test data, but I want to understand things that come standard in Rails before moving onto other things.
Can anyone help me get this working?
UPDATE:
I managed to get the test to fail in a different location, I had to ensure that the test could actually get to the create action (have some authlogic stuff in my app)
it now says
undefined method 'users' for nil:Class
So now it's saying that it can't find a users collection on my #account object in the controller, basically because it still can't find the #account even though it doesn't actually fail to find the account in question. So my before_filter :find_account works to the extent that it doesn't break, but it seem to not be finding the account.
I tried the post :create, :id => #account.id, :user => { } but to no avail. Also tried post :create, :account => accounts(:one), :user => { } and :user => { :account => accounts(:one) }, again with the same result.
I think you got the association backwards. If that's the case then Account model should belong_to :user, then User model should has_one :account.
Otherwise, since you're creating user which belongs to some account, you should pass account's :id in params:
post :create, :id => some_test_account.id, :user => {...}
The typical controller for the belongs_to side of a has_many association would have a create action like this:
def create
#account = Account.find(params[:account_id])
#user = #account.users.build(params[:user])
if #account.save
# handle success
else
# handle failure
end
end
If your controller doesn't look like this you not be handling the parameters correctly, thus the test failure.
You can also check your routes. You should see something like:
POST /accounts/:account_id/users(.:format) {:controller=>"users", :action=>"create"}
That's another clue that Rails is setting params[:account_id] to the value for the requested account.
I am having a problem in RSpec when my mock object is asked for a URL by the ActionController. The URL is a Mock one and not a correct resource URL.
I am running RSpec 1.3.0 and Rails 2.3.5
Basically I have two models. Where a subject has many notes.
class Subject < ActiveRecord::Base
validates_presence_of :title
has_many :notes
end
class Note < ActiveRecord::Base
validates_presence_of :title
belongs_to :subject
end
My routes.rb file nests these two resources as such:
ActionController::Routing::Routes.draw do |map|
map.resources :subjects, :has_many => :notes
end
The NotesController.rb file looks like this:
class NotesController < ApplicationController
# POST /notes
# POST /notes.xml
def create
#subject = Subject.find(params[:subject_id])
#note = #subject.notes.create!(params[:note])
respond_to do |format|
format.html { redirect_to(#subject) }
end
end
end
Finally this is my RSpec spec which should simply post my mocked objects to the NotesController and be executed... which it does:
it "should create note and redirect to subject without javascript" do
# usual rails controller test setup here
subject = mock(Subject)
Subject.stub(:find).and_return(subject)
notes_proxy = mock('association proxy', { "create!" => Note.new })
subject.stub(:notes).and_return(notes_proxy)
post :create, :subject_id => subject, :note => { :title => 'note title', :body => 'note body' }
end
The problem is that when the RSpec post method is called.
The NotesController correctly handles the Mock Subject object, and create! the new Note object. However when the NoteController#Create method tries to redirect_to I get the following error:
NoMethodError in 'NotesController should create note and redirect to subject without javascript'
undefined method `spec_mocks_mock_url' for #<NotesController:0x1034495b8>
Now this is caused by a bit of Rails trickery that passes an ActiveRecord object (#subject, in our case, which isn't ActiveRecord but a Mock object), eventually to url_for who passes all the options to the Rails' Routing, which then determines the URL.
My question is how can I mock Subject so that the correct options are passed so that I my test passes.
I've tried passing in :controller => 'subjects' options but no joy.
Is there some other way of doing this?
Thanks...
Have a look at mock_model, which is added by rspec-rails to make it easier to mock ActiveRecord objects. According to the api docs:
mock_model: Creates a mock object instance for a model_class with common methods stubbed out.
I'm not sure if it takes care of url_for, but it's worth a try.
Update, 2018-06-05:
As of rspec 3:
mock_model and stub_model have been extracted into the rspec-activemodel-mocks gem.
In case zetetic's idea doesn't work out, you can always say Subject.new and then stub out to_param and whatever else you might need faked for your example.
I have two models:
class Solution < ActiveRecord::Base
belongs_to :owner, :class_name => "User", :foreign_key => :user_id
end
class User < ActiveRecord::Base
has_many :solutions
end
with the following routing:
map.resources :users, :has_many => :solutions
and here is the SolutionsController:
class SolutionsController < ApplicationController
before_filter :load_user
def index
#solutions = #user.solutions
end
private
def load_user
#user = User.find(params[:user_id]) unless params[:user_id].nil?
end
end
Can anybody help me with writing a test for the index action? So far I have tried the following but it doesn't work:
describe SolutionsController do
before(:each) do
#user = Factory.create(:user)
#solutions = 7.times{Factory.build(:solution, :owner => #user)}
#user.stub!(:solutions).and_return(#solutions)
end
it "should find all of the solutions owned by a user" do
#user.should_receive(:solutions)
get :index, :user_id => #user.id
end
end
And I get the following error:
Spec::Mocks::MockExpectationError in 'SolutionsController GET index, when the user owns the software he is viewing should find all of the solutions owned by a user'
#<User:0x000000041c53e0> expected :solutions with (any args) once, but received it 0 times
Thanks in advance for all the help.
Joe
EDIT:
Thanks for the answer, I accepted it since it got my so much farther, except I am getting another error, and I can't quite figure out what its trying to tell me:
Once I create the solutions instead of build them, and I add the stub of the User.find, I see the following error:
NoMethodError in 'SolutionsController GET index, when the user owns the software he is viewing should find all of the solutions owned by a user'
undefined method `find' for #<Class:0x000000027e3668>
It's because you build solution, not create. So there are not in your database.
Made
before(:each) do
#user = Factory.create(:user)
#solutions = 7.times{Factory.create(:solution, :owner => #user)}
#user.stub!(:solutions).and_return(#solutions)
end
And you mock an instance of user but there are another instance of User can be instanciate. You need add mock User.find too
before(:each) do
#user = Factory.create(:user)
#solutions = 7.times{Factory.create(:solution, :owner => #user)}
User.stub!(:find).with(#user.id).and_return(#user)
#user.stub!(:solutions).and_return(#solutions)
end
I figured out my edit, when a find is done from the params, they are strings as opposed to actual objects or integers, so instead of:
User.stub!(:find).with(#user.id).and_return(#user)
I needed
User.stub!(:find).with(#user.id.to_s).and_return(#user)
but thank you so much shingara you got me in the right direction!
Joe