Rails: how to test before_save callbacks - ruby-on-rails

Given a typical ActiveRecord model, I often have before_save callbacks that parse input, for instance taking something like time_string from the user and parsing it into a time field.
That setup might look like this:
before_save :parse_time
attr_writer :time_string
private
def parse_time
time = Chronic.parse(time_string) if time_string
end
I understand that it's considered best practice to make callback methods private. However, if they're private, then you can't call them individually to test them in isolation.
So, for you seasoned Rails testers out there, how do you handle testing this kind of thing?

In Ruby, Private methods are still available via Object#send
You can exploit this for your unit testing like so:
project = Project.new
project.time_string = '2012/11/19 at Noon'
assert_equal(project.send(:parse_time), '2012-11-19 12:00:00')

What I would do is save the state of an new or build instance of your object, save the object and make the assertion or expectation based on the value of the attribute that was changed by before_save
post = Post.new
post.time_string = '2012/11/19'
expected_time = Chronic.parse(post.time_string)
post.save
assert_equal(post.time, expected_time)
That way you are testing the behavior of how the object should act and not necessarily the implementation of the method.

There are times when I have an if condition on my callbacks in which case I use run_callbacks.
before_save :parse_time, :if => Proc.new{ |post| post.foo == 'bar' }
is tested positively by
post = Post.new
post.foo = 'bar'
expected_time = Chronic.parse(post.time_string)
post.run_callbacks :before_save
assert_equal(post.time, expected_time)
and negatively by
post = Post.new
post.foo = 'wha?'
post.run_callbacks :before_save
assert_nil(post.time)
See the API and a blog for more details.

Related

rails 5 update attribute only if it's currently nil

What is the preferred way in Rails 5 with activerecord to update the attribute only if it is currently nil.
car = Car.first
car.connected_at = Time.zone.now
car.save
OR
car = Car.first
car.update!(connected_at: Time.zone.now)
it should update only if car.connected_at is nil
You can simply check for #nil?
car = Car.first
car.update_attribute(:connected_at, Time.zone.now) if car.connected_at.nil?
That's not generic enough. I want something like before_validation etc. I am just not sure which way is the preferred one.
Well if you want to go for validation, it would look something like this..
before_save :validate_connected_at
private
def validate_connected_at
connected_at = connected_at_was if connected_at_changed? && connected_at_was.present?
end
OR
before_save :set_connected_at
private
def set_connected_at
connected_at = Time.zone.now if connected_at.nil?
end
As you can see, more checks, more methods. I would definitely go for the first one.
However, if you want to add error message, then this is the way
errors.add(:connected_at, 'Already present!')
So "#{attr}_was" is always available on all the defined attrs in before_save method?
They are available in general and not only in before_save, e.g. in the console..
car = Car.first
car.connected_at
=> 'some value'
car.connected_at = 'some other value'
car.connected_at
=> 'some other value'
car.connected_at_was
=> 'some value'
It sounds like you're saying you want to modify the behaviour of how a particular attribute works so it quietly ignores you. I think the instinct behind why you want to seal this off is reasonable one but if you think about it a bit more you might consider that if you do this kind of thing in a lot of places then using your objects will start to become confusing particularly for someone else who doesn't know the code well.
Perhaps you want to do this because there's other code using the Car model that wants to make connections but doesn't really have the full picture so it tries stuff which you only want to succeed the first time. It's much better to handle such operations solely inside a class which does have the full picture such as the Car model or a service object.
If you still really want to control this "connecting" behaviour outside the Car then you can override the attr_writer completely in the Car class. I'd definitely recommend doing this on before_save callback instead though.
def connected_at=(new_value)
if #connected_at
raise StandardError, 'connected_at has already been set'
end
#connected_at = new_value
end
That will work whichever way you try to assign the value. If you're wondering about what's going on above have a read about attr_accessor in ruby.
this is my understanding of your question.
Car can update only if connected_at is nil
class Car < ApplicationRecord
before_save :updatable?
def updatable?
connected_at.blank?
end
end
The point is return false when before_save.
You could:
car = Car.first
car.connected_at ||= Time.zone.now
car.save
That will only assign if connected_at is nil of false.
I would propose to use the before_update callback and rephrase the intention of the OP as "discard updates if my attribute already has a value".
I came up with this solution (which works well with mass assignments such as Car.update(car_params)):
before_update :ignore_updates_to_connected_at
def ignore_updates_to_connected_at
return unless connected_at.present? && connected_at_changed?
clear_attribute_change(:connected_at)
end
The <attribute_name>_changed? and clear_attribute_change methods come from ActiveModel::Dirty.

Using conditionals on callbacks rails

I have a callback on my comment model that creates a notification that gets sent out to the appropriate members but I don't want it to create a notification if the current_member is commenting on his own commentable object. I've tried using the unless conditional like this:
after_create :create_notification, on: :create, unless: Proc.new { |commentable| commentable.member == current_member }
def create_notification
subject = "#{member.user_name}"
body = "wrote you a <b>Comment</b> <p><i>#{content}</i></p>"
commentable.member.notify(subject, body, self)
end
But I get this error: undefined local variable or method 'current_member' for #<Comment:0x746e008
How do I get this to work like I want?
It's pretty atypical to try to use current_user or things like that from the model layer. One problem is that you're really coupling your model layer to the current state of the controller layer, which will make unit testing your models much more difficult and error-prone.
What I would recommend is to not use an after_create hook to do this, and instead create the notifications at the controller layer. This will give you access to current_user without needing to jump through any hoops.

Can I make Rails update_attributes with nested form find existing records and add to collections instead of creating new ones?

Scenario: I have a has_many association (Post has many Authors), and I have a nested Post form to accept attributes for Authors.
What I found is that when I call post.update_attributes(params[:post]) where params[:post] is a hash with post and all author attributes to add, there doesn't seem to be a way to ask Rails to only create Authors if certain criteria is met, e.g. the username for the Author already exists. What Rails would do is just failing and rollback update_attributes routine if username has uniqueness validation in the model. If not, then Rails would add a new record Author if one that does not have an id is in the hash.
Now my code for the update action in the Post controller becomes this:
def update
#post = Post.find(params[:id])
# custom code to work around by inspecting the author attributes
# and pre-inserting the association of existing authors into the testrun's author
# collection
params[:post][:authors_attributes].values.each do |author_attribute|
if author_attribute[:id].nil? and author_attribute[:username].present?
existing_author = Author.find_by_username(author_attribute[:username])
if existing_author.present?
author_attribute[:id] = existing_author.id
#testrun.authors << existing_author
end
end
end
if #post.update_attributes(params[:post])
flash[:success] = 'great!'
else
flash[:error] = 'Urgg!'
end
redirect_to ...
end
Are there better ways to handle this that I missed?
EDIT: Thanks for #Robd'Apice who lead me to look into overriding the default authors_attributes= function that accepts_nested_attributes_for inserts into the model on my behalf, I was able to come up with something that is better:
def authors_attributes=(authors_attributes)
authors_attributes.values.each do |author_attributes|
if author_attributes[:id].nil? and author_attributes[:username].present?
author = Radar.find_by_username(radar_attributes[:username])
if author.present?
author_attributes[:id] = author.id
self.authors << author
end
end
end
assign_nested_attributes_for_collection_association(:authors, authors_attributes, mass_assignment_options)
end
But I'm not completely satisfied with it, for one, I'm still mucking the attribute hashes from the caller directly which requires understanding of how the logic works for these hashes (:id set or not set, for instance), and two, I'm calling a function that is not trivial to fit here. It would be nice if there are ways to tell 'accepts_nested_attributes_for' to only create new record when certain condition is not met. The one-to-one association has a :update_only flag that does something similar but this is lacking for one-to-many relationship.
Are there better solutions out there?
This kind of logic probably belongs in your model, not your controller. I'd consider re-writing the author_attributes= method that is created by default for your association.
def authors_attributes=(authors_attributes)
authors_attributes.values.each do |author_attributes|
author_to_update = Author.find_by_id(author_attributes[:id]) || Author.find_by_username(author_attributes[:username]) || self.authors.build
author_to_update.update_attributes(author_attributes)
end
end
I haven't tested that code, but I think that should work.
EDIT: To retain the other functionality of accepts_nested_Attributes_for, you could use super:
def authors_attributes=(authors_attributes)
authors_attributes.each do |key, author_attributes|
authors_attributes[key][:id] = Author.find_by_username(author_attributes[:username]).id if author_attributes[:username] && !author_attributes[:username].present?
end
super(authors_attributes)
end
If that implementation with super doesn't work, you probably have two options: continue with the 'processing' of the attributes hash in the controller (but turn it into a private method of your controller to clean it up a bit), or continue with my first solution by adding in the functionality you've lost from :destroy => true and reject_if with your own code (which wouldn't be too hard to do). I'd probably go with the first option.
I'd suggest using a form object instead of trying to get accepts_nested_attributes to work. I find that form object are often much cleaner and much more flexible. Check out this railscast

Base.save, callbacks and observers

Let us say that we have the model Champ, with the following attributes, all with default values of nil: winner, lose, coach, awesome, should_watch.
Let's assume that two separate operations are performed: (1) a new record is created and (2) c.the_winner is called on a instance of Champ.
Based on my mock code, and the observer on the model, what values are saved to the DB for these two scenarios? What I am trying to understand is the principles of how callbacks work within the context of Base.save operation, and if and when the Base.save operation has to be called more than once to commit the changes.
class Champ
def the_winner
self.winner = 'me'
self.save
end
def the_loser
self.loser = 'you'
end
def the_coach
self.coach = 'Lt Wiggles'
end
def awesome_game(awesome_or_not=false)
self.awesome = awesome_or_not
end
def should_watch_it(should=false)
self.should_watch = should
end
end
class ChampObserver
def after_update(c)
c.the_loser
end
def after_create(c)
c.the_coach
end
def before_create(c)
c.awesome_game(true)
c.should_watch_it(true) if c.awesome_game
end
end
With your example, if you called champ.winner on a new and unmodified instance of Champ, the instance of Champ would be committed to the DB and would look like this in the database:
winner: 'me'
awesome: true
should_watch: true
loser: nil
coach: nil
The after_create callback would be called if it is a new record, and if not, the after_update callback would (this is why loser would be nil if the instance was new). However, because they just call a setter method on the instance, they will only update the instance and will not commit more changes to the DB.
You could use update_attribute in your observer or model methods to commit the change, but unless you actually need to have the record in the database and then update it, it's wasteful. In this example, if you wanted those callbacks to actually set loser and coach in the database, it'd be more efficient to use before_save and before_create.
The Rails guides site has a good overview of callbacks here, if you haven't read it already.

How can I test that my before_save callback does the right thing

I have a callback on my ActiveRecord model as shown below:
before_save :sync_to_external_apis
def sync_to_external_apis
[user, assoc_user].each {|cuser|
if cuser.google_refresh
display_user = other_user(cuser.id)
api = Google.new(:user => cuser)
contact = api.sync_user(display_user)
end
}
end
I would like to write an rspec test which tests that calling save! on an instance of this model causes sync_user to be called on a new Google instance when google_refresh is true. How could I do this?
it "should sync to external apis on save!" do
model = Model.new
model.expects(:sync_to_external_apis)
model.save!
end
As an aside, requesting unreliable resources like the internet during the request-response cycle is a bad idea. I would suggest creating a background job instead.
The usual method for testing is to ensure the results are as expected. Since you're using an API in this case that may complicate things. You may find that using mocha to create a mock object you can send API calls would allow you to substitute the Google class with something that works just as well for testing purposes.
A simpler, yet clunkier approach is to have a "test mode" switch:
def sync_to_external_apis
[ user, assoc_user ].each do |cuser|
if (Rails.env.test?)
#synced_users ||= [ ]
#synced_users << cuser
else
# ...
end
end
end
def did_sync_user?(cuser)
#synced_users and #synced_users.include?(cuser)
end
This is a straightforward approach, but it will not validate that your API calls are being made correctly.
Mocha is the way to go. I'm not familiar with rspec, but this is how you would do it in test unit:
def test_google_api_gets_called_for_user_and_accoc_user
user = mock('User') # define a mock object and label it 'User'
accoc_user = mock('AssocUser') # define a mock object and label it 'AssocUser'
# instantiate the model you're testing with the mock objects
model = Model.new(user, assoc_user)
# stub out the other_user method. It will return cuser1 when the mock user is
# passed in and cuser2 when the mock assoc_user is passed in
cuser1 = mock('Cuser1')
cuser2 = mock('Cuser2')
model.expects(:other_user).with(user).returns(cuser1)
model.expects(:other_user).with(assoc_user).returns(cuser2)
# set the expectations on the Google API
api1 - mock('GoogleApiUser1') # define a mock object and lable it 'GoogleApiUser1'
api2 - mock('GoogleApiUser2') # define a mock object and lable it 'GoogleApiUser2'
# call new on Google passing in the mock user and getting a mock Google api object back
Google.expects(:new).with(:user => cuser1).returns(api1)
api1.expects(:sync_user).with(cuser1)
Google.expects(:new).with(:user => cuser2).returns(api2)
api2.expects(:sync_user).with(cuser2)
# now execute the code which should satisfy all the expectations above
model.save!
end
The above may seem complicated, but it's not once you get the hang of it. You're testing that when you call save, your model does what it is supposed to do, but you don't have the hassle, or time expense of really talking to APIs, instantiating database records, etc.

Resources