Ruby on Rails gotchas

This page documents my discovery of oddities that briefly suprised me when getting to grips with Ruby on Rails.

Testing

Fixture instance variables not defined

If you get a nil object when you expect a fixture instance variable, check that you've set the self.use_instantiated_fixtures = true in test/test_helper.rb.

assert_raise doesn't catch the exception

It's not meant to! Just make sure you don't have any failures.

Errors are not cumulative

When an object's save method fails (perhaps due to failed validation), its errors array (or hash or whatever) is populated with the errors that occurred. They are not cumulative, so when you call the save method again the previous errors are cleared before new ones can be generated.

Transactional tables

If you use MySQL MyISAM tables and you have the self.use_transactional_fixtures = true in test/test_helper.rb, Rails will not be able to roll back the database after each test method and you will get Errors when one method is called after another that's destroyed a required object.

Either set the variable to false or use transactional (InnoDB) tables. The latter is recommended because it'll result in faster tests.

Active Record

Validation, instance variables and databases updates

A model will allow its variables to be changed, despite validation, but the database will only be updated if the save method is called and validation passes. In other words, the model can change despite validation failures, but the database can not.

New model with many -> 1 table relationship fails unit test

I have 2 models, accounts and posts, that have a many to 1 relationship. An account has many posts and a post has 1 account. So, Account has_many :posts and Post belongs_to :account.

A scaffold was generated for each model. The account unit test works fine, the post unit test fails with the following errors:

user@localhost ~/wcs/rails_experiments/test $ ruby test/unit/post_test.rb 
Loaded suite test/unit/post_test
Started
EE
Finished in 0.06319 seconds.

  1) Error:
test_truth(PostTest):
ActiveRecord::StatementInvalid: Mysql::Error: Cannot add or update a child row: a foreign key constraint fails (`test_test/posts`, CONSTRAINT `fk_posts_account` FOREIGN KEY (`account_id`) REFERENCES `accounts` (`id`)): INSERT INTO posts (`id`) VALUES (2)
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/connection_adapters/abstract_adapter.rb:120:in `log'
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/connection_adapters/mysql_adapter.rb:184:in `execute'
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/fixtures.rb:288:in `insert_fixtures'
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/fixtures.rb:287:in `each'
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/fixtures.rb:287:in `insert_fixtures'
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/fixtures.rb:257:in `create_fixtures'
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/fixtures.rb:257:in `each'
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/fixtures.rb:257:in `create_fixtures'
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/connection_adapters/abstract/database_statements.rb:51:in `transaction'
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/fixtures.rb:255:in `create_fixtures'
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/base.rb:794:in `silence'
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/fixtures.rb:248:in `create_fixtures'
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/fixtures.rb:565:in `load_fixtures'
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/fixtures.rb:512:in `setup'

  2) Error:
test_truth(PostTest):
NoMethodError: You have a nil object when you didn't expect it!
You might have expected an instance of Array.
The error occured while evaluating nil.-
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/transactions.rb:112:in `unlock_mutex'
    /usr/lib/ruby/gems/1.8/gems/activerecord-1.14.4/lib/active_record/fixtures.rb:534:in `teardown'

1 tests, 0 assertions, 0 failures, 2 errors

The post unit test fails because its default fixture (test/fixtures/posts.yml) contains 2 items, neither of which contain the data required to respect its many -> 1 relationship with the accounts table.

Commenting out the items, or populating the fixture with sensible data, will fix the problem:

user@localhost ~/wcs/rails_experiments/test $ ruby test/unit/post_test.rb 
Loaded suite test/unit/post_test
Started
.
Finished in 0.029939 seconds.

1 tests, 1 assertions, 0 failures, 0 errors

:dependent => :delete_all doesn't work in test mode

If you have an object with a has_many :posts, :dependent => :delete_all relationship with another object, submitting the destroy action via Webrick running in test mode does not delete the dependent objects from the database. It works fine in development mode.

AJAX and RJS

link_to_remote graceful degredation

By default, link_to_remote doesn't gracefully degrade when Javascript is disabled because it uses href='#' instead of linking to the required action. However, you can specify html options, including the href. So, instead of this...

<%= link_to_remote 'Do Whatever', :url => { :controller => 'the_controller', :action => 'the_action' }, :update => 'element_to_update' %>

...do this:

<%= link_to_remote 'Do Whatever', { :url => { :controller => 'the_controller', :action => 'the_action' }, :update => 'element_to_update' }, { :href => url_for(:controller => 'the_controller', :action => 'the_action') } %>

Alternatively, you could use the form_remote_tag method:

<%= form_remote_tag :url => { :controller => 'the_controller', :action => 'the_action' }, :update => 'element_to_update' %><%= submit_tag 'Do Whatever' %><%= end_form_tag %>

See DRYing up link_to_remote for degradable URLs and Working through Agile Web Development with Rails \u2013 Part 4.

insertHtml is not a function

You can not use the JavascriptGenerator insert_html method to insert HTML into a table. You will receive a Javascript alert similar to the following:

RJS error:

TypeError: ${"object_id").insertHtml is not a function

RJS conditionals

Turns out there are no decend conditionals in RJS. You either have to iterate through a collection using the each method or use a nasty raw Javascript hack. See RJS Rails If statement and Javascript Conditionals in RJS.

RJS remove method

If you have a div that contains nothing...

<div id='outer_div'></div>

...then the following Javascript condition will evaluate as false because the div has no contents:

if (document.getElementById('element_id').innerHTML) {...}

You can then insert HTML into this div from an RJS template, using the objects provided by Rails' JavaScriptGenerator:

page['outer_div'].replace_html '<div id=\"inner_div\">blah</div>'

And remove it using the remove method:

page['inner_div'].remove

However, the remove method leaves behind a space (or line break - I'm not sure which yet). You can see it in the Firefox DOM Inspector as a #text node. This means that document.getElementById('element_id').innerHTML has a value and hence won't return false as it did previously. In other words the remove method doesn't remove everything.

Note that this problem could be unique to Firefox (see Bug 26179), but the fact remains that the remove method does not return the element to the state it was in before the sub-element was added. I've not figured out the solution to this yet.

Last modified: 25/04/2007 Tags: (none)

This website is a personal resource. Nothing here is guaranteed correct or complete, so use at your own risk and try not to delete the Internet. -Stephan

Site Info

Privacy policy

Go to top