Dynamically add data accessor methods on “static” Rails data model

An useful metaprogramming spell I recently played with is the Module#define_method(), which dynamically adds an instance method to the class on which is called.

 

I found it particularly useful to add data accessor methods on “static” Rails data model: suppose I’m working an e-commerce Rails webapp, and I have a Country model which maps the countries suitable for shipping, or a PaymentType model which represents all the possible payment types.

For these kind of models (and tables), which are typically static (they don’t change often), you often have to access specific values, say Country.italy or PaymentType.credit_card.

In these cases, defining dynamically an accessor method may be useful and more clear than always perform a find_by_name("my value").

So, for example, I open up my country.rb model class and add these lines

class << self
  Country.all.each do |each_country|
    define_method(each_country.name.downcase.gsub('.', '').gsub(' ', '_')) do
      Country.find_by_iso_code(each_country.iso_code)
    end
  end
end

And then opening the Rails console I will be able to type

1.8.7@epistore > Country.sri_lanka
# {
                :id => 59,
              :zone => "U9",
           :enabled => true,
        :created_at => Tue, 20 Apr 2010 17:01:45 CEST +02:00,
        :updated_at => Tue, 20 Apr 2010 17:01:45 CEST +02:00,
          :iso_code => "LK",
    :country_set_id => nil
}

Just a note: as I said, Module#define_method() will add an instance method on the class. To add a class method, which is what I want, we have to use a different approach, using the class << self syntax to add a singleton method in the receiver.

I may also add a query method on each Country instance to check that country against another country (for example, I may ask my_country.italy?)

  Country.all.each do |each_country|
    define_method(each_country.name.downcase.gsub('.', '').gsub(' ', '_').concat('?')) do
      has_iso_code? each_country.iso_code
    end
  end

And then, after issuing a reload! command in the Rails console, I may type:

1.8.7@epistore > Country.usa.usa?
true
1.8.7@epistore > Country.usa.italy?
false
1.8.7@epistore > Country.usa.south_korea?
false
1.8.7@epistore > Country.south_korea.south_korea?
true

Depending on the kind of Rails app you have, these may be a useful tip.

assert_select_rjs reloaded!

If you ever dared to unit-test a Rails RJS action, for example something like this:
def my_ajax_action
   ...
   render(:update) do |page|
     page.replace_html 'shoppinglist', :partial => 'cart'
     page.replace_html 'items', :partial => 'layouts/items', :locals => { :cart => @cart }
   end
end
you may already know and use the assert_select_rjs testing helper, which basically will verify the structure of your RJS response.

This testing method may really help you shortening the TDD feedback loop in an AJAX-based Rails webapp, and then you’ll may even be confident enough and save one or two brittle Selenium tests.

The only problem with assert_select_rjs is that is (IMHO) poorly documented and rarely googled about.
So, this is my turn to give back what we discovered.

If you have a Rails webapp using jQuery as javascript framework, you may have a hard time using assert_select_rjs correctly, and this is why:

for jQuery, this is the correct way to use assert_select_rjs:
assert_select_rjs :replace_html, '#shoppinglist'
it’s important the ‘#’ prefix here to refer to DOM element IDs, since the notation without ‘#’ will work only if your app uses Prototype.
Another nice thing to know is the way to make assertion on the selection matched by the assert_select_rjs.
For example, this code
assert_select_rjs :replace_html, '#shoppinglist' do
    assert_select '#shipping_cost_description', /Shipping costs for France/
    assert_select '#shipping_cost_value', /&euro; 12,30/
end
will verify that the section replaced inside the ‘shoppinglist’ element will match the two followings assetions.

My first test using webdriver (aka Selenium 2.0)!

As many say, a good solution to selenese flu is Webdriver (see more at http://code.google.com/p/selenium).

Webdriver has been accepted by the Selenium guys as the new approach to web application testing, opposed to the classical “selenium 1.0″ approach, based on a javascript driver, which suffers from way too many issues.
Unfortunately, Selenium 2.0, which plan to fully support Webdriver, is still on an alpha release, and actually is very difficult to find ruby-based web testing tools supporting this alpha version of selenium 2.0.
One of those tools is actually Watir (though Webrat too is planning to support Selenium 2.0 sooner or later), and more precisely this project is quite stable to allow a first test drive.

So this is what I did:

First: installed required gems

  sudo gem install selenium-webdriver
  sudo gem install watir-webdriver --pre

Second: configure my Rails testing configuration to use watir

config/environments/test.rb
  ...
  config.gem "watir-webdriver"
  ...
test/test_helper.rb
  require 'test_help'
  ...
  require 'watir-webdriver'
  ...

Third: write a test

test/integration/paypal_integration_test.rb
require 'test_helper'

class PaypalIntegrationTest < ActionController::IntegrationTest
  include LocaleHelper
  self.use_transactional_fixtures = false

  def setup
    ... some setup stuff here ...   
    @browser = Watir::Browser.new(:firefox)
  end

  def teardown
    @browser.close
  end

  test "something interesting" do
    @browser.goto "https://developer.paypal.com/"
    @browser.text_field(:name, "login_email").set "my_test_account@sourcesense.com"
    @browser.text_field(:name, "login_password").set "mysecret"
    @browser.button(:name, "submit").click

    @browser.goto "https://localhost"

    @browser.link(:id, 'loginlink').click
    @browser.text_field(:name, "email").set @user.email
    @browser.text_field(:name, "password").set @user.password
    @browser.button(:text, "Login").click

    # add_a_product_to_cart
    product = Factory(:product, :code => "a code", :categories => [@juve_store])
    Factory(:product_variant, :code => "03", :availability => 99, :product => product)
    @browser.goto "https://localhost/frontend/products/show/#{product.id}"
    @browser.button(:id, "add_to_cart").click

    @browser.link(:text, "Checkout").click
    @browser.link(:id, "gotobuy").click

    # choose "Paypal"
    @browser.radios.last.set

    @browser.link(:id, "gotobuy").click

    sleep 5
    assert @browser.text.include?("Payment for order #{last_order_number()}")

    @browser.text_field(:name, "login_email").set "my_test_buyer@sourcesense.com"
    @browser.text_field(:name, "login_password").set "yetanothersecrethere"
    @browser.button(:text, "Accedi").click
    @browser.button(:text, "Paga ora").click

    sleep 5
    assert @browser.text.include?("Il pagamento è stato inviato")

    @browser.button(:id, "merchantReturn").click
    assert_contain_waiting("Your purchase")
    assert_contain_waiting(last_order_number())

  end

private

  def last_order_number
    Order.last ? Order.last.number : ""
  end

end

Some comments here:

  • This is a spike, so please don’t say this test is too long and not well refactored
  • I had to put two sleep calls in two places (I gotta say that this specific test, involving paypal sandbox, is really slow due to the slowness in the paypal response time).
  • Anyway, this alpha version of webdriver is still lacking: I cannot say wheather this is a problem I’ll have even with future (possibly more stable) version of Webdriver.

Some references:

A (still brief) experience on using Selenium to test a Rails + ajax app

This is a note to make a point on our (mine and my team’s) current use of Selenium to test the ajax behaviour in the Rails webapp we’re currently developing. Ajax replacing of part of the page is growing, and with it we have to face the classical question: “how do we test (I mean automatically :-) the ajax/javascript behaviours in our webapp?”.

This is how we are trying to manage this issue now, after some days of spiking on Selenium, Watir and BlueRidge (I hope to write more on Watir and BlueRidge in some future post, because these two tools are worth speaking).

Actually we are giving a try to the combination of Webrat + Selenium, since we already have a big test suite of integration test using Webrat, and have a good knowledge of the Webrat API.

We added the selenium-client gem to be able to drive Selenium through the Webrat API.
This is extracted from our test environment configuration file:

test.rb
...
config.gem 'selenium-client', :lib => 'selenium/client'
config.gem "webrat", :version => '>= 0.6.0'
...

Then, we defined a class from which all the selenium test cases will inherit.
This class basically is used to

  • disable the transactional fixtures in Rails, to allow the browser process where Selenium runs to access the data prepared in the tests
  • configure Webrat with the “selenium” mode
  • be the place to collect helper methods as “login” or “logout”, used in many tests.
selenium_integration_test.rb
class SeleniumIntegrationTest < ActionController::IntegrationTest
  self.use_transactional_fixtures = false

  setup :switch_webrat_to_selenium
  def switch_webrat_to_selenium
    Webrat.configure do |config|
      config.mode = :selenium
      config.application_environment = :test
    end

    selenium.set_speed(100)       # default is 0 ms
    selenium.set_timeout(10000)   # default is 30000 ms
  end

  teardown :delete_cookies
  def delete_cookies
    selenium.delete_all_visible_cookies
  end

protected
 ...
 [other helper methods here, like login, logout, and so on...]

 ...

We also added a rake task to be able to launch all the selenium tests

test.rake
namespace :test do
  ...
  ...

  desc "Run Selenium Test"
  Rake::TestTask.new(:selenium) do |t|
    t.libs << "test"
    t.test_files = FileList['test/selenium/*test.rb']
    t.verbose = true
  end
end

One thing we learned through several repeated mistakes is that the Webrat API is different when called in the “selenium” mode then the one we were used to when using Webrat in the classical “rails” mode.
For example, the “assert_have_selector” method for selenium only takes one argument, that is the CSS selector, while in the classical webrat mode, the same method takes another parameter to specify the expected content to match with (see this rdoc: http://gitrdoc.com/brynary/webrat/tree/master). So we had to define helper methods based on “assert_have_xpath” method using xpath to express the same intent of a method like assert_have_selector(css_selector, expected_content)

Here is our helper method

selenium_integration_test.rb
  ...
  def assert_has_id id, text_content
    assert_have_xpath "//*[@id='#{id}'][1][text()='#{text_content}']"
  end
  ...

Fixing SeleniumRC to work with Firefox 3.6

The brand new release of Firefox 3.6 brings, together with some improvements in the browser, also some headaches for all selenium users: actually the latest selenium RC jar (selenium-server.jar) won’t work with Firefox 3.6.

The problem is related to the addons that Selenium will pretend to have in the Firefox instance fired up when Selenium RC server starts. As a matter of fact, those two addons are not declared to be compatible with 3.6.

The simple fix is then to edit the addons’ install.rdf files in the selenium-server.jar to manually set the compatibility to 3.6.

Alternatively, you can download this patched jar from this repository, rename it to selenium-server.jar and replace the previous jar with this.

The actual steps to fix my webrat gem (I use Selenium through Webrat) were

  1. download the above mentioned file (http://github.com/saucelabs/saucelenium/blob/master/selenium-sauce.jar)
  2. rename it to selenium-server.jar
  3. replace the previous file in the vendor folder of your webrat gem (mine was /usr/local/lib/ruby/gems/1.8/gems/webrat-0.7.0/vendor/selenium-server.jar)

One (and a half) useful thing to know when using DeepTest gem with MySQL

DeepTest currently won’t work if you’ve configured MySQL with no password (in other words, if you are able to connect to mysql with a simple “mysql -u root”).
To fix this, you have to patch DeepTest (I know, asap I’ll go through the whole process to propose the patch to the original project leader).
Actually, you have to comment out a line, in the DeepTest:Database:MysqlSetupListener#grant_privileges method:

...
def grant_privileges(connection)
sql = %{grant all on #{worker_database}.*
to %s@'localhost';} % [
connection.quote(worker_database_config[:username])# ,
# connection.quote(worker_database_config[:password])  <-- mysql with no password won't work
]
connection.execute sql
end
...

Another tip (the “half” in the blog post title):
Don’t forget to edit the “pattern” option in your DeepTest rake task, to be able to grab all the testcases you want.
In my case, I want to skip a whole folder containing selenium tests, so I have to write my Deep Test rake file this way:
(in /lib/tasks/test.rake)

require "deep_test/rake_tasks"
...

DeepTest::TestTask.new "deep" do |t|
t.number_of_workers = 2
t.pattern = "test/{unit,functional,integration}/**/*_test.rb"
t.libs << "test"
t.worker_listener = "DeepTest::Database::MysqlSetupListener"
end