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.
Related
I'm trying to implement retweet functionality on my app.
So I have my retweet_id in my tweets model
tweets schema
| user_id | content | created_at | updated_at | retweet_id
tweets.rb
belongs_to :user
has_many :retweets, class_name: 'Tweet', foreign_key: 'retweet_id'
user.rb
has_many :tweets
And in my tweets controller
tweets_controller.rb
...
def retweet
#retweet = Tweet.new(retweet_params)
if #retweet.save
redirect_to tweet_path, alert: 'Retweeted!'
else
redirect_to root_path, alert: 'Can not retweet'
end
end
Private
...
def retweet_params
params.require(:retweet).permit(:retweet_id, :content).merge(user_id: current_user.id)
end
In my view
tweets/show.html.erb
<%= link_to 'Retweet', retweet_tweet_path(#tweet.id), method: :post %>
My routes
resources :tweets do
resources :comments
resources :likes
member do
post :retweet
end
end
So when I try this I get an error
param is missing or the value is empty: retweet
So I remove .require from 'retweet_params' and that removes that error (though i'm unsure of how wise that is)
Then the link works but won't retweet - reverting to the fallback root_path specified in my action instead.
Unpermitted parameters: :_method, :authenticity_token, :id
Redirected to http://localhost:3000/
I'm not sure what i'm doing wrong. How can I get my retweets working? ty
The reason retweet_params raises an error is because your link link_to 'Retweet', retweet_tweet_path(#tweet.id), method: :post doesn't contain parameters like a new or edit form does. Instead you should create a new tweet that reference to tweet you want to retweet.
before_action :set_tweet, only: %i[show edit update destroy retweet]
def retweet
retweet = #tweet.retweets.build(user: current_user)
if retweet.save
redirect_to retweet, notice: 'Retweeted!'
else
redirect_to root_path, alert: 'Can not retweet'
end
end
private
def set_tweet
#tweet = Tweet.find(params[:id])
end
The above should automatically link the new tweet to the "parent". If this doesn't work for some reason you could manually set it by changing the above to:
retrweet = Tweet.new(retweet_id: #tweet.id, user: current_user)
The above approach doesn't save any content, since this is a retweet.
If you don't want to allow multiple retweets of the same tweet by the same user, make sure you have the appropriate constraints and validations set.
# migration
add_index :tweets, %i[user_id retweet_id], unique: true
# model
validates :retweet_id, uniqueness: { scope: :user_id }
How do we access the content of a retweet? The answer is we get the content form the parent or source (however you want to call it).
There is currently no association that lets you access the parent or source tweet. You currently already have:
has_many :retweets, class_name: 'Tweet', foreign_key: 'retweet_id'
To easily access the source content let's first add an additional association.
belongs_to :source_tweet, optional: true, inverse_of: :retweets, class_name: 'Tweet', foreign_key: 'retweet_id'
has_many :retweets, inverse_of: :source_tweet, class_name: 'Tweet', foreign_key: 'retweet_id'
With the above associations being set we can override the content getter and setter of the Tweet model.
def content
if source_tweet
source_tweet.content
else
super
end
end
def content=(content)
if source_tweet
raise 'retweets cannot have content'
else
super
end
end
# depending on preference the setter could also be written as validation
validates :content, absence: true, if: :source_tweet
Note that the above is not efficient when talking about query speed, but it's the easiest most clear solution. Solving parent/child queries is sufficiently difficult that it should get its own question, if speed becomes an issue.
If you are wondering why I set the inverse_of option. I would recommend you to check out the section Active Record Associations - 3.5 Bi-directional Associations.
Right now the error you're seeing is the one for strong params in Rails. If you can check your debugger or the HTTP post request that's being sent, you'd find that you don't have the params that you're "requiring" in retweet_params
def retweet_params
params.require(:retweet).permit(:retweet_id, :content).merge(user_id: current_user.id)
end
This is essentially saying that you expect a nested hash for the params like so
params = { retweet: { id: 1, content: 'Tweet' } }
This won't work since you're only sending the ID. How about something like this instead?
TweetsController.rb
class TweetsController < ApplicationController
def retweet
original_tweet = Tweet.find(params[:id])
#retweet = Tweet.new(
user_id: current_user.id,
content: original_tweet.content
)
if #retweet.save
redirect_to tweet_path, alert: 'Retweeted!'
else
redirect_to root_path, alert: 'Can not retweet'
end
end
end
I have an (I think) relatively straightforward has_many :through relationship with a join table:
class User < ActiveRecord::Base
has_many :user_following_thing_relationships
has_many :things, :through => :user_following_thing_relationships
end
class Thing < ActiveRecord::Base
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
end
class UserFollowingThingRelationship < ActiveRecord::Base
belongs_to :thing
belongs_to :user
end
And these rspec tests (I know these are not necessarily good tests, these are just to illustrate what's happening):
describe Thing do
before(:each) do
#user = User.create!(:name => "Fred")
#thing = Thing.create!(:name => "Foo")
#user.things << #thing
end
it "should have created a relationship" do
UserFollowingThingRelationship.first.user.should == #user
UserFollowingThingRelationship.first.thing.should == #thing
end
it "should have followers" do
#thing.followers.should == [#user]
end
end
This works fine UNTIL I add an after_save to the Thing model that references its followers. That is, if I do
class Thing < ActiveRecord::Base
after_save :do_stuff
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
def do_stuff
followers.each { |f| puts "I'm followed by #{f.name}" }
end
end
Then the second test fails - i.e., the relationship is still added to the join table, but #thing.followers returns an empty array. Furthermore, that part of the callback never gets called (as if followers is empty within the model). If I add a puts "HI" in the callback before the followers.each line, the "HI" shows up on stdout, so I know the callback is being called. If I comment out the followers.each line, then the tests pass again.
If I do this all through the console, it works fine. I.e., I can do
>> t = Thing.create!(:name => "Foo")
>> t.followers # []
>> u = User.create!(:name => "Bar")
>> u.things << t
>> t.followers # [u]
>> t.save # just to be super duper sure that the callback is triggered
>> t.followers # still [u]
Why is this failing in rspec? Am I doing something horribly wrong?
Update
Everything works if I manually define Thing#followers as
def followers
user_following_thing_relationships.all.map{ |r| r.user }
end
This leads me to believe that perhaps I am defining my has_many :through with :source incorrectly?
Update
I've created a minimal example project and put it on github: https://github.com/dantswain/RspecHasMany
Another Update
Thanks a ton to #PeterNixey and #kikuchiyo for their suggestions below. The final answer turned out to be a combination of both answers and I wish I could split credit between them. I've updated the github project with what I think is the cleanest solution and pushed the changes: https://github.com/dantswain/RspecHasMany
I would still love it if someone could give me a really solid explanation of what is going on here. The most troubling bit for me is why, in the initial problem statement, everything (except the operation of the callback itself) would work if I commented out the reference to followers.
I've had similar problems in the past that have been resolved by reloading the association (rather than the parent object).
Does it work if you reload thing.followers in the RSpec?
it "should have followers" do
#thing.followers.reload
#thing.followers.should == [#user]
end
EDIT
If (as you mention) you're having problems with the callbacks not getting fired then you could do this reloading in the object itself:
class Thing < ActiveRecord::Base
after_save { followers.reload}
after_save :do_stuff
...
end
or
class Thing < ActiveRecord::Base
...
def do_stuff
followers.reload
...
end
end
I don't know why RSpec has issues with not reloading associations but I've hit the same types of problems myself
Edit 2
Although #dantswain confirmed that the followers.reload helped alleviate some of the problems it still didn't fix all of them.
To do that, the solution needed a fix from #kikuchiyo which required calling save after doing the callbacks in Thing:
describe Thing do
before :each do
...
#user.things << #thing
#thing.run_callbacks(:save)
end
...
end
Final suggestion
I believe this is happening because of the use of << on a has_many_through operation. I don't see that the << should in fact trigger your after_save event at all:
Your current code is this:
describe Thing do
before(:each) do
#user = User.create!(:name => "Fred")
#thing = Thing.create!(:name => "Foo")
#user.things << #thing
end
end
class Thing < ActiveRecord::Base
after_save :do_stuff
...
def do_stuff
followers.each { |f| puts "I'm followed by #{f.name}" }
end
end
and the problem is that the do_stuff is not getting called. I think this is the correct behaviour though.
Let's go through the RSpec:
describe Thing do
before(:each) do
#user = User.create!(:name => "Fred")
# user is created and saved
#thing = Thing.create!(:name => "Foo")
# thing is created and saved
#user.things << #thing
# user_thing_relationship is created and saved
# no call is made to #user.save since nothing is updated on the user
end
end
The problem is that the third step does not actually require the thing object to be resaved - its simply creating an entry in the join table.
If you'd like to make sure that the #user does call save you could probably get the effect you want like this:
describe Thing do
before(:each) do
#thing = Thing.create!(:name => "Foo")
# thing is created and saved
#user = User.create!(:name => "Fred")
# user is created BUT NOT SAVED
#user.things << #thing
# user_thing_relationship is created and saved
# #user.save is also called as part of the addition
end
end
You may also find that the after_save callback is in fact on the wrong object and that you'd prefer to have it on the relationship object instead. Finally, if the callback really does belong on the user and you do need it to fire after creating the relationship you could use touch to update the user when a new relationship is created.
UPDATED ANSWER **
This passes rspec, without stubbing, running callbacks for save (after_save callback included ), and checks that #thing.followers is not empty before trying to access its elements. (;
describe Thing do
before :each do
#user = User.create(:name => "Fred");
#thing = Thing.new(:name => 'Foo')
#user.things << #thing
#thing.run_callbacks(:save)
end
it "should have created a relationship" do
#thing.followers.should == [#user]
puts #thing.followers.inspect
end
end
class Thing < ActiveRecord::Base
after_save :some_function
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
def some_function
the_followers = followers
unless the_followers.empty?
puts "accessing followers here: the_followers = #{the_followers.inspect}..."
end
end
end
ORIGINAL ANSWER **
I was able to get things to work with the after_save callback, so long as I did not reference followers within the body / block of do_stuff. Do you have to reference followers in the real method you are calling from after_save ?
Updated code to stub out callback. Now model can remain as you need it, we show #thing.followers is indeed set as we expected, and we can investigate the functionality of do_stuff / some_function via after_save in a different spec.
I pushed a copy of the code here: https://github.com/kikuchiyo/RspecHasMany
And spec passing thing* code is below:
# thing_spec.rb
require 'spec_helper'
describe Thing do
before :each do
Thing.any_instance.stub(:some_function) { puts 'stubbed out...' }
Thing.any_instance.should_receive(:some_function).once
#thing = Thing.create(:name => "Foo");
#user = User.create(:name => "Fred");
#user.things << #thing
end
it "should have created a relationship" do
#thing.followers.should == [#user]
puts #thing.followers.inspect
end
end
# thing.rb
class Thing < ActiveRecord::Base
after_save :some_function
has_many :user_following_thing_relationships
has_many :followers, :through => :user_following_thing_relationships, :source => :user
def some_function
# well, lets me do this, but I cannot use #x without breaking the spec...
#x = followers
puts 'testing puts hear shows up in standard output'
x ||= 1
puts "testing variable setting and getting here: #{x} == 1\n\t also shows up in standard output"
begin
# If no stubbing, this causes rspec to fail...
puts "accessing followers here: #x = #{#x.inspect}..."
rescue
puts "and this is but this is never seen."
end
end
end
My guess is that you need to reload your Thing instance by doing #thing.reload (I'm sure there's a way to avoid this, but that might get your test passing at first and then you can figure out where you've gone wrong).
Few questions:
I don't see you calling #thing.save in your spec. Are you doing that, just like in your console example?
Why are you calling t.save and not u.save in your console test, considering you're pushing t onto u? Saving u should trigger a save to t, getting the end result you want, and I think it would "make more sense" considering you are really working on u, not t.
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
I'm having a problem testing the following model:
class Bill < ActiveRecord::Base
belongs_to :consignee
before_save :calc_rate
def calc_rate
self.chargeableweight = self.consignee.destination.rate * self.weight
end
end
The consignee model:
class Consignee < ActiveRecord::Base
belongs_to :destination
has_many :bills
end
The controllers are not touched yet.
The behavior of the app is correct (follow up question: are there any performance problems with that solution?) - but the the test break.
You have a nil object when you didn't
expect it! You might have expected an
instance of Array. The error occurred
while evaluating nil.*
Thank you in advice,
Danny
update:
This bill test breaks using factory girl:
describe Bill do
it "should call the calc_rate method" do
bill = Factory.build(:bill)
bill.save!
bill.should_receive(:calc_rate)
end
end
You have a nil object when you didn't expect it!
Factories:
Factory.define :destination do |f|
f.airport_code "JFK"
end
Factory.define :consignee do |f|
...
f.association :destination
end
Factory.define :bill do |f|
f.association :consignee
f.weight 10
f.chargeableweight 20.0
f.after_create do |bill|
bill.calc_rate
end
describe Consignee do
it "should calculate the rate" do
#pending
#make sure this spec is passing first, so you know your calc_rate method is fine.
end
it "should accept calc_rate before save" do
cosignee = mock("Consignee")
consignee.should_receive(:calc_rate).and_return(2) # => stubbing your value
end
end
I didn't spool up a rails app to test this code, but this should get you close. also, assuming that the columns chargeable_rate, weight, etc are columns on the model, you dont need to call self. Ruby will implicitly expect self if there is no instance method or variable of that name available it will automatically look for class methods.
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.