Enabling/disabling observers for testing 1
If you use ActiveRecord observers in your application and are concerned about the isolation of your model unit tests, you probably want some way to disable/enable observers. Unfortunately, Rails doesn't provide an easy way to do this. So, here's some code I threw together a while ago to do just that.
module ObserverTestHelperMethods
def observer_instances
ActiveRecord::Base.observers.collect do |observer|
observer_klass = \
if observer.respond_to?(:to_sym)
observer.to_s.camelize.constantize
elsif observer.respond_to?(:instance)
observer
end
observer_klass.instance
end
end
def observed_classes(observer=nil)
observed = Set.new
(observer.nil? ? observer_instances : [observer]).each do |observer|
observed += (observer.send(:observed_classes) + observer.send(:observed_subclasses))
end
observed
end
def observed_classes_and_their_observers
observers_by_observed_class = {}
observer_instances.each do |observer|
observed_classes(observer).each do |observed_class|
observers_by_observed_class[observed_class] ||= Set.new
observers_by_observed_class[observed_class] << observer
end
end
observers_by_observed_class
end
def disable_observers(options={})
except = options[:except]
observed_classes_and_their_observers.each do |observed_class, observers|
observers.each do |observer|
unless observer.class == except
observed_class.delete_observer(observer)
end
end
end
end
def enable_observers(options={})
except = options[:except]
observer_instances.each do |observer|
unless observer.class == except
observed_classes(observer).each do |observed_class|
observer.send :add_observer!, observed_class
end
end
end
end
end
Include this in a Test::Unit::TestCase or 'include' in your RSpec configuration, whatever rocks your boat. Here's a stupid example:
class SomethingCoolTest < Test::Unit::TestCase
include ObserverTestHelperMethods
def setup
disable_observers
end
def teardown
enable_observers
end
def test_without_observers
# ...
end
end
When you go to test the behavior of the observer itself, simply disable/enable like the following to disable/enable all observers except the one you're testing:
class DispassionateObserverTest < Test::Unit::TestCase
include ObserverTestHelperMethods
def setup
disable_observers :except => DispassionateObserver
end
def teardown
enable_observers :except => DispassionateObserver
end
def test_without_observers_except_dispassionate_observer
# ...
end
end
Plugin to Support composed_of Aggregations in ActiveRecord Finder Methods 1
In Rails, hooking up an ActiveRecord model to use a value object to aggregate over a set of database fields is a piece of cake. With the accessor methods that are created for a composed_of association, you can now deal exclusively with the composed_of field on your model, instead of directly manipulating or querying the individual database fields that it aggregates. Or can you? As long as all you're doing with the aggregate field is getting and setting its value, your aggregated database fields remain encapsulated. However, if you want to retrieve instances of your model from the database through a call to a finder method, you must do so on the individual database fields.
Consider the following ActiveRecord model definition:
class Customer < ActiveRecord::Base
composed_of :balance, :class_name => "Money", :mapping => %w(balance_amount amount)
end
Given such a model, we can do something like this with no problem:
customer = Customer.new
customer.balance = Money.new(512.08)
customer.balance # returns #<Money:abc @amount=512.08>
customer.save!
However, now that we've saved the record, we might want to get that record back from the database at some point with code that looks something like:
customer = Customer.find(:all, :conditions => { :balance => Money.new(512.08) })
or like:
customer = Customer.find_all_by_balance( Money.new(512) )
This would provide full encapsulation of the aggregated database fields for the purposes of both record creation and retrieval. The problem is, at the time of my posting this article, it doesn't work. Instead, you have to do this:
customer = Customer.find(:all, :conditions => { :balance_amount => 512.08 })
To deal with this problem, I've submitted a ticket, which is currently scheduled to be available in Rails 2.1.
If you need this functionality, but your project is using a pre-2.1 release of Rails, I've also created a plugin version of the changes I submitted in the aforementioned ticket. To install:
script/plugin install http://svn.ryankinderman.net/find_conditions_with_aggregation
Addendum: The patch has been committed to changeset 8671. Yay!
Selenium Core Bug and TinyMCE Anchor Tags
Today, I was trying to get Selenium to click an anchor tag that was created with the "link" plugin in a TinyMCE editor. I was able to verify that the link was present with something as simple as verifyElementPresent('link=Link Text'). However, when I tried calling clickAndWait('link=Link Text'), it gave a "Window does not exist" error. A quick Google search yielded the answer: a bug in Selenium Core.
When the TinyMCE "link" plugin creates a link that doesn't open in a new window, it sets the "target" attribute on the anchor tag to "_self". Selenium Core versions prior to 0.8.4 (which hasn't been released yet) don't respond to links with "target" set to "_self".
If you're doing Rails development and using the selenium_on_rails plugin, it uses an old version of Selenium Core (0.7.something) as of this posting. To fix the anchor tag problem, I replaced the contents of the selenium-core directory under vendor/plugins/selenium_on_rails with that of the core directory of the Selenium Core 0.8.3 release, then applied the patch described in the bug spec linked to above. This seems to have fixed the problem.
Hopefully this saves you all some time and muddling.
Passing Arrays and Nested Params to url_for in Rails 9
A few weeks ago (okay, more than a few weeks ago, it took me a while to write this), I discussed the problems involved with passing nested hash parameters to named routes in Rails. My coding pair and I discovered another bug (still using rev 5522) when passing hash parameters to a named route in Rails, this time when the hash contains arrays. For example, consider the following call to a named route:
person_url(:name => ['Ryan', 'Kinderman']) "http://someurl.com/people?name[]=Ryan&name[]=Kinderman"Unfortunately, it gets encoded to look like this:
"http://someurl.com/people?name=Ryan%2FKinderman"For those of you unfamiliar with CGI escaping, the %2F translates into the '/' character. So, you end up with a
params hash in the controller where params[:name] == ['Ryan/Kinderman']. How disappointing. To get around this in the past, I've chosen to either split the hash value on '/', or use my own encoding of arrays that Rails can handle, and then simply decode them myself within the controller. In the above example, I could have done something like:
person_url(:name => {0 => 'Ryan', 1 => 'Kinderman}) What I present here is a detailed explanation of the problem, with instructions at the end on how to install my plugin patch to fix it. My explanation and patch address the issues for both nested and array parameters. There are a number of methods involved in the solution to this problem. It may be useful at this point for you to refer to Jamis Buck's excellent articles on the gory details of Rails route recognition and generation.
The Rails methods involved in processing route parameters are listed below, in their general call sequence:
ActionController::Base.url_forin action_controller/base.rb
OR
ActionView::Helpers::UrlHelper#link_toin action_view/helpers/url_helper.rbRouteSet#generatein action_controller/routing.rbRouteSet#options_as_paramsin action_controller/routing.rbArray#to_paramin active_support/core_ext/array/conversions.rbRoute#generatein action_controller/routing.rbRoute#write_generationin action_controller/routing.rbRoute#append_query_stringin action_controller/routing.rbRoute#build_query_stringin action_controller/routing.rb
link_to or url_for, either explicitly or through the named route *_url methods, they roughly follow the call sequence depicted in the following diagram to generate the query string for the resulting URL:
The problems start in the call to options_as_params. This method is not recursive, and processing nested parameters is a recursive problem. The next issue with options_as_params is not actually in the method, but in the to_param method that it calls. If you look at the Rails implementation of Array#to_param, you'll see that all it's doing is joining the elements into a '/' separated string. This doesn't get processed back into separate array elements when the request is received by the controller. So, in the case when value is an Array instance during a call to options_as_params, the resulting string is encoded incorrectly.
The other specific issue lies in the Route#build_query_string method. Take a look at the method, and notice the part that looks like:
if value.class == Array
key << '[]'
else
value = [ value ]
end url_for as an option parameter when that array comes from the params hash from within a controller action (*whew*, that was a mouthful!). This is because what you thought was an array is actually an instance of ActionController::Routing::PathSegment::Result. To be honest, I don't know why this is happening. I looked at the code and realized that it'd take me longer to figure out than what I wanted to spend at the time. However, if someone could explain it to me, I'd love to hear it. In any case, to solve this particular problem, the conditional needs to be changed from a check for only Array to Array and any subclasses using something like the is_a? method.
So, those are the issues involved in why array and nested hash parameters don't work properly in calls to url_for. Rather than going through my solution, I'm offering it as a Rails plugin with full unit test coverage, and plan to submit it as an actual patch to the Rails team, with the code cleaned up a bit more. Maybe there are reasons why this sort of thing isn't supported, but I can't think what they might be. I'll post updates here if and when I get more information on this. If you have comments or questions on this patch or parts of the code, please let me know.
You can install the Rails plugin by typing the following into your command-line:
ruby script/plugin install http://svn.ryankinderman.net/nested_params_patch
To see the issues I've discussed first-hand, after installing the plugin, take a look at controller_test.rb.
Addendum: I checked, and as of revision 6141 of Rails, the issues covered by this article are still present, and the plugin still fixes them.
Addendum (2007/04/03): I've just got around to confirming that, as rwd's commented, the bug has been fixed. If you're using revision 6343 or later of Rails, you probably aren't going to need this patch. Yay!
Never Raise StandardError Directly
It's not a good idea to raise StandardError directly from your Rails application, particularly when using an exception as a way to indicate a recoverable error from an ActiveRecord transaction. In such cases, it's better to subclass from StandardError and raise the subclass instead. If you use StandardError directly, you may find that your system is catching and recovering from errors that are related to faulty logic rather than broken business rules.
For example, consider the following bad code:
1 2 3 4 5 6 7 8 9 10 11 | |
rescue clause above is thrown by the code on line 2. In fact, the actual error thrown by this line is an instance of NoMethodError, which is a subclass of NameError, which is a subclass of StandardError.
If you wrote a unit test for the some_method method, and asserted that StandardError was raised, it would pass incorrectly. This is especially misleading if you are writing a test for a controller action and asserting that, upon some error condition, the application redirects the user to a page displaying that error. I find that I don't explicitly raise many errors from my application code, except when I wrap the code in a controller action inside an ActiveRecord transaction. The only way to roll back the transaction is to raise an exception from within the transaction and rescue from it outside, such as:
class SomeController < ApplicationController
def some_action
begin
SomeRecordClass.transaction do
a = [1, 2]
some_method( a )
end
rescue StandardError
@error_message = "Bad value for index 0: #{a[0]}"
render 'some_template'
return
end
redirect_to some_url
end
end Nested Hash Params with Named Routes in Ruby on Rails 3
I've never (as of rev 5522) been able to pass a nested hash as parameters to url_for and have it flatten it properly on the generated URL. I encountered the problem again the other day and, having had just about enough, decided to try and nip it in the bud.
The problem I was having occurred when tried to make an HTTP request to a named route such as:
people_url(:person => { :name => 'Bob', :profession => 'Developer' }) When I've encountered this problem in the past, I could never find any information from someone who's actually solved the problem. There are a few patches submitted as tickets to the Rails devs, but one seems to be a duplicate of the other, and neither seems to fix this problem on Edge, at least not for my particular use case.
The root of this particular problem lies in a method in the action_controller/routing.rb file within actionpack/actioncontroller, in the options_as_params of the RoutingSet class. The method, without comments, looks like this:
1 2 3 4 5 6 7 | |
value is a Hash. In that case, the to_param method simply converts the Hash instance into its String representation, which is just a pile of keys and values mashed together, as in my people_url example.
To solve this problem, I changed the method to look like this:
def options_as_params(options)
options_as_params = options[:controller] ? { :action => "index" } : {}
options.parameterize do |param_key, param_value|
options_as_params[param_key] = param_value.to_param
end
options_as_params
end parameterize method is an extension I made to the Hash class that essentially flattens a hash like:
{
:person => {
:name => 'Bob',
:profession => 'Developer'
}
} {
:'person[name]' => 'Bob',
:'person[profession]' => 'Developer'
} class Hash
def flatten(superkey)
flattened_hash = {}
self.each do |key, value|
flattened_hash["#{superkey}[#{key}]".to_sym] = value
end
flattened_hash
end
def parameterize
self.each do |k, value|
if value.is_a?(Hash)
value.flatten(k).each { |fk, fv| yield fk, fv }
else
yield k, value
end
end
end
end 