Hoopla!

now with extra whiz-bang!

Hoopla!

Sinatra, Merb, and Camping

October 18, 2007 · 5 comments

I found Camping first. It was hard to figure out because Google kept offering me places to vacation. Then I saw Merb. I was too busy shaving yaks to ever give it a real go. Now there’s Sinatra. It’s probably worth learning because it shouldn’t take more than 5 minutes.

I love that Ruby makes kickass websites in so many colors.

Thanks to DFisch for the heads up.

→ 5 comments Tags:

Counting with scope_out (or: extending Rails)

August 08, 2007 · 3 comments

I figure it’s time I write a post with some guts to it. One thing I keep hearing people ask about is how to write code that extends Rails. Most of the books and tutorials out there (RailsCasts and PeepCode excluded) focus on getting you up and running but don’t show you how to really master your application.

So for folks who want to get better at Rails here’s a not uncommon situation involving external libraries and how to extend them cleanly. Using this technique you can easily add any kind of functionality to your program.

I’m finally using scope_out in an app and so far I’m loving it. I’d written some code that acted similarly to the scope_out plugin but it was not nearly as good. I’ve decided to replace that code with the plugin because the smaller I can get my own application code the better.

Scope out lets you define extensions to your normal finders. For example:

class Book < ActiveRecord::Base
  scope_out :rated, :conditions => 'rating is not null'
end
That gives you:

Book.find_rated(:all) # == Book.find(:all, :conditions => 'rating is not null')
Book.calculate_rated(:count, :all) # == Book.calculate(:count, :all, :conditions => 'rating is not null')

Which is pretty handy. In fact it’s beautiful. It’s so good that Rick Olson even recommends it over his acts_as_paranoid plugin.

But my old code had the ability to count records like:

Book.count_rated
. I find myself doing this all the time. I suppose I could just replace my calls to
Book.count_rated
with
Book.calculate_rated(:count, :all)
but that would add an ugly level of complexity.

As I see it I have some options for how to get the counting functionality back in my app:
  1. I can submit a patch to the scope_out project to get my favorite method added to the code.
  2. I can manually edit my installation of the plugin.
  3. I can add a method_missing to ActiveRecord::Base that intercepts count_* and reformats the call to be used with scope_out
  4. I can add an extension to the scope_out plugin under my /lib directory that modifies the plugin without having to actually modify the plugin’s installed files.

So which is best?

#1 is the best idea but it’ll take a while and they might not accept it. Oddly enough I’m too lazy to submit a patch but I found time for this blog post.
#2 is a bad idea. I don’t want to support this plugin myself – I want the folks upstream to do all the work for me.
#3 might depend on the scope_out plugin depending how I write it. It seems like a clean solution otherwise.
#4 Kinda like #2 in that I’ll have to make sure my code is working fine every time I update the scope_out plugin. Too much work.

I’m going to go with #3. It’s the cleanest overall and will cause the fewest headaches down the road.

This kind of code could be made into a plugin but since I don’t plan to use this in any other apps yet I should start with the simplest option – which is to create a new file under ./lib and require it in my environment.rb file.


$ touch ./lib/scoped_count_extension.rb
$ echo "require 'scoped_count_extension'" >> ./config/envirionment.rb

Crafting the Ruby

If I wanted to be a total bum I could just do it like this:

class ActiveRecord::Base
  def count_rated(*args)
    calculate_rated(:count, :all, *args)
  end
end
and that would kinda sorta solve my problem. Technically it would work and I could now use Book.count_rated(). But it would only work for the rated_scope – for any other scope I’d have to duplicate this code. And every agile developer I know would pummel me if I did that. So I’m going to have to use method_missing.

class ActiveRecord::Base
  def method_missing(method_called, *args, &block)
    match = method_called.to_s.match(/^count_(\w+)$/)
    if match
      send "calculate_#{match[1]}", :count, :all, *args
    end
  end
end

Hmmm, it’s working but I think I’d rather only do the count -> calculate replacement if I know that with_scope is going to catch it. So I should check for with_rating or something like it because that’s a method that scope_out provides.


class ActiveRecord::Base
  def method_missing(method_called, *args, &block)
    match = method_called.to_s.match(/^count_(\w+)$/)
    if match && respond_to?("with_#{match[1]}")
      send "calculate_#{match[1]}", :count, :all, *args
    end
  end
end

There. That means that anytime a method gets called that looks like ‘count_something’ on any model class and there’s a ‘with_something’ defined (which means that scope_out is applied to this ‘something’) it’ll pass it along to calculate_something(:count, :all). The match() method is just performing a simple regular expression so I can capture the bit that comes after count_ and put calculate_ before it.

But now I’ve got a serious problem. Method_missing is a really handy method and it’s already used by ActiveRecord pretty heavily. So all I’ve really managed to do is break Rails. Things like Book.find_by_title rely on method_missing and I’d like to keep that behavior.

Thankfully Rails provides us with an incredibly useful tool called alias_method_chain. It’s a way to chain together a bunch of methods that all have the same name. As long as each one has a unique identifier they won’t clobber each other.

So I’ll alias_method_chain my method_missing:

class ActiveRecord::Base
  def method_missing_with_scope_out_countable(method_called, *args, &block)
    match = method_called.to_s.match(/^count_(\w+)$/)
    if match && respond_to?("with_#{match[1]}")
      send "calculate_#{match[1]}", :count, :all, *args
    else
      method_missing_without_scope_out_countable(method_called, *args, &block)
    end
  end
  alias_method_chain :method_missing, :scope_out_countable
end

Much better. Now my code can work right alongside the rest of the Rails source. There’s just one more ‘gotcha’: this code reopens ActiveRecord::Base and just adds itself in there. This is affectionately called “monkey-patching” and it’s frowned upon when it can be avoided. So I’m going to rearrange this code to make it kinder to the class it’s modifying.

First I move this extended method_missing into a module of it’s own:

module ScopeOutCountable
  def method_missing_with_scope_out_countable(method_called, *args, &block)
    match = method_called.to_s.match(/^count_(\w+)$/)
    if match && respond_to?("with_#{match[1]}")
      send "calculate_#{match[1]}", :count, :all, *args
    else
      method_missing_without_scope_out_countable(method_called, *args, &block)
    end
  end
end

class ActiveRecord::Base
end
Then I move just the the stuff that I want to become part of ActiveRecord::Base into a submodule of it’s own:

module ScopeOutCountable
  module ClassMethods
    def method_missing_with_scope_out_countable(method_called, *args, &block)
      match = method_called.to_s.match(/^count_(\w+)$/)
      if match && respond_to?("with_#{match[1]}")
        send "calculate_#{match[1]}", :count, :all, *args
      else
        method_missing_without_scope_out_countable(method_called, *args, &block)
      end
    end
  end
end

class ActiveRecord::Base
end
Next I go ahead and include the top-level module into ActiveRecord:

module ScopeOutCountable
  module ClassMethods
    def method_missing_with_scope_out_countable(method_called, *args, &block)
      match = method_str.match(/^count_(\w+)$/)
      if match && respond_to?("with_#{match[1]}")
        send "calculate_#{match[1]}", :count, :all, *args
      else
        method_missing_without_scope_out_countable(method_called, *args, &block)
      end
    end
  end
end

class ActiveRecord::Base
end
Now, to get it actually working I’ll need to add an install hook that connects my method_missing to ActiveRecord::Base

module ScopeOutCountable

  # included() gets executed anytime this module is included into an object.  The object is passed as an argument to included().
  class << self
    def included(target)
      target.extend ClassMethods
    end
  end

  module ClassMethods
    def method_missing_with_scope_out_countable(method_called, *args, &block)
      match = method_str.match(/^count_(\w+)$/)
      if match && respond_to?("with_#{match[1]}")
        send "calculate_#{match[1]}", :count, :all, *args
      else
        method_missing_without_scope_out_countable(method_called, *args, &block)
      end
    end
  end
end

class ActiveRecord::Base
  include ScopeOutCountable
end

There. I’ve now got my custom method_missing call hooked up to a chain of other method_missings within ActiveRecord::Base. I can use it as simply as this:


Book.find_rated(:all) # == Book.find(:all, :conditions => 'rating is not null')
Book.calculate_rated(:count, :all) # == Book.calculate(:count, :all, :conditions => 'rating is not null')
Book.count_rated # == Book.count(:conditions => 'rating is not null')
And for those who are curious how this might be really easily turned into a plugin:

$ ruby ./script/generate plugin scope_out_countable
$ mv ./lib/scope_out_countable.rb /vendor/plugins/scope_out_countable/lib/scope_out_countable.rb
$ echo "require 'scope_out_countable'" > /vendor/plugins/scope_out_countable/init.rb
$ sed -i "s/require 'scope_out_countable'//" ./config/environment.rb # this is a lazy way of deleting the require line from environment.rb

Update: If anyone can explain to me why passing off to method_missing_without_scope_out_countable would completely bypass scope_out’s method_missing I’d love to hear about it.

→ 3 comments Tags:

Advanced Rails Creation Script

February 10, 2007 · 0 comments

Akhil Bansal recently wrote two excellent scripts for creating new Rails projects. The first was in bash (a sensible choice) and the second was a Ruby port of the same code. I've taken his code one step further and developed it into a Ruby program that's easier to modify to your own needs.

Take the following code, throw it on your system somewhere and run it. It'll set up a Rails app and take care of all the nutty details like ignoring log files and removing those pesky tmp and components directories from your repository.

#!/usr/bin/env ruby
require 'fileutils'

class LayRails

  attr_accessor :path, :name, :svn_username, :svn_url, :svn_password

  def initialize
    setup && lay
  end

  def setup

    unless system('rails -v') || system('rails.cmd -v')
      puts "Cannot find rails. Terminating..."
      exit 0
    end    

    self.path = enter "Enter Rails Application Path:(eg: /home/akhil/ror): "
    self.name = ARGV.first || path.split('/').last
    self.svn_username = enter  "Enter svn username: "
    self.svn_password = enter "Enter the svn password: "
    self.svn_url = enter "Enter the svn url: "

    puts "
      ---------------------------------------
      Please verify the following variables: 
      Svn Username: #{svn_username}
      Svn URL: #{svn_url}
      Svn Password: #{svn_password.gsub(/./, '*')}
      Application Path: #{path}
      Application name: #{name}"

    while yes_no = enter("Proceed (y/n)").strip.upcase
      return true if 'Y' == yes_no
      if 'N' == yes_no
        puts 'Terminating...'
        exit 0
      else
        puts 'Please enter either a "y" or a "n"'
        puts 'yes_no: '+yes_no.inspect
      end
    end
  end

  def lay

    puts  "Generating rails project: (#{path})"
    @windows ?
      psystem("rails.cmd #{f path}") :
      psystem("rails #{f path}")

    puts  "SVNinitial import: "
    puts "svn import #{path} #{svn_url}/trunk -m \"Initial Import\" --username #{svn_username} --password #{svn_password.gsub(/./, '*')}"
    system "svn import #{path} #{svn_url}/trunk -m \"Initial Import\" --username #{svn_username} --password #{svn_password}"

    FileUtils.remove_dir(path, true)

    puts  "Checking out from svn: "

    psystem "svn checkout #{svn_url}/trunk #{path}"
    FileUtils.cd(path, :verbose => true)

    remove f('log/*'), 'Removing all log files from SVN'
    ignore '*', 'log', 'Ignoring all log files under log dir'
    remove 'tmp', 'Removing tmp directory from SVN'
    remove 'components', 'Removing components directory from SVN'
    ignore 'tmp', '.', 'Ignoring tmp dir'

    puts  "Generating optimized database file"
    File.open(f('config/database.yml'), 'w') {|f| f.write(database_yml_file) }
    puts  "Moving database.yml to database.example"
    psystem f('svn move config/database.yml config/database.example')
    psystem 'svn commit -m "Moving database.yml to database.example to provide a template for anyone who checks out the code " '
    ignore 'database.yml', 'config', 'Ignoring database.yml'
    puts  "Finished."
  end

  def remove(what, log)
    psystem "svn remove #{what}"
    commit log
  end

  def ignore(what, where, log)
    psystem "svn propset svn:ignore '#{what}' #{where}"
    psystem "svn update"
    commit log
  end

  def commit(log)
    psystem "svn commit -m '#{log}'"
  end

  def f(string)
    @windows ||= RUBY_PLATFORM =~ /mswin/i
    @windows ? 
      string.gsub('/', '\\') : 
      string
  end

  def psystem(string)
    puts string
    system string
  end

  def enter(string)
    puts string
    gets.strip
  end

  def database_yml_file
    <

production:
  <<: *credentials
development:
  <<: *credentials
  database: #{name}_development
test:
  <<: *credentials
  database: #{name}_test
DBYML
  end

  def credits
    puts < - based on the work of Akhil Bansal"
CREDITS
  end

end

# and away we go
LayRails.new

→ 0 comments Tags:

Amazon S3 Backup via Rails Plugin

January 02, 2007 · 13 comments

I think Amazon S3 is awesome. I was looking into building a RAID NAS (Network-Attached Storage) for backing up all my important data and I nearly bought a setup that would run into the hundreds of dollars - but then I did a little fancy multiplication and addition and realized S3 would cost me less than one one-hundredth what the NAT would have cost.

In case you’re as in the dark about S3 as I recently was, here’s a little rundown: it’s a very simple, super fast, extremely large backup system that Amazon uses for all of it’s own storage needs. It’s opened the service up to the public on a pay-as-you-go basis.

Costs:

  • $0.15 per GB-Month of storage used
  • $0.20 per GB of data transferred

In other words, it’s damn cheap. Say you want to upload 500 MS Word documents that are around 45KB each? How much would it cost to store those on some easy-to-access, highly secure, permanent backup place? Less than one cent per month. Eight years later you’d only be out a half-dollar ($0.32 to be precise).

So S3 is my new storage/backup location of choice. The one difficulty of using it is that I need to figure out some way of automating the backups so that the backups are actually useful and can easily be recovered if necessary. In particular, I need some way to automatically backup the website data that is so crucial to me.

So I made a plugin.

The S3 plugin will allow you to backup your crucial website data to S3 via a handy Rake task (written by the talented Adam Greene).

Amazon has been an excellent supporter of Ruby/Rails lately (they fund 43things.com among other things) and they’ve made sure to release a ruby library for S3. I’ve combined that with Adam’s S3 rake task into a handy S3 backup plugin.

You can install it via the following two commands:

1
2
ruby script/plugin source http://svn.6brand.com/projects/plugins
ruby script/plugin install -x s3

Then backing up is easy as:

1
2
3
rake s3:backup:db
rake s3:backup:code
rake s3:backup:scm

or, to get them all together:

1
rake s3:backup

→ 13 comments Tags:

"Warning: colon will be obsoleted; use semicolon"

August 23, 2006 · 9 comments

If you’re getting the title of this post as a strange error showing up in your rails app you’re not alone. If you happen to see this you’re probably running a new version of Ruby (1.8.5).

The source behind this error is in the syntax for Ruby’s control structures (particularly using case and if. Here’s an example of some code from the Rails trunk that follows the currently popular style:

active_record/base.rb:

      def convert_number_column_value(value)
        case value
          when FalseClass: 0
          when TrueClass:  1
          when '':         nil
          else value
        end
      end
action_view/base.rb:

      def find_template_extension_for(template_path)
        if match = delegate_template_exists?(template_path)
          match.first.to_sym
        elsif erb_template_exists?(template_path):        :rhtml
        elsif builder_template_exists?(template_path):    :rxml
        elsif javascript_template_exists?(template_path): :rjs
        else
          raise ActionViewError, "No rhtml, rxml, rjs or delegate template found for #{template_path} in #{@base_path}" 
        end
      end

If you’re getting the title of this post as a strange error showing up in your rails app you’re not alone. If you happen to see this you’re probably running a new version of Ruby (1.8.5).

The source behind this error is in the syntax for Ruby’s control structures (particularly using case and if. Here’s an example of some code from the Rails trunk that follows the currently popular style:

active_record/base.rb:

      def convert_number_column_value(value)
        case value
          when FalseClass: 0
          when TrueClass:  1
          when '':         nil
          else value
        end
      end
action_view/base.rb:

      def find_template_extension_for(template_path)
        if match = delegate_template_exists?(template_path)
          match.first.to_sym
        elsif erb_template_exists?(template_path):        :rhtml
        elsif builder_template_exists?(template_path):    :rxml
        elsif javascript_template_exists?(template_path): :rjs
        else
          raise ActionViewError, "No rhtml, rxml, rjs or delegate template found for #{template_path} in #{@base_path}" 
        end
      end

The warning comes from the use of colons to separate a condition from a statement. Apparently Ruby is working toward eliminating the use of this colon in favor of a semicolon. Since a semicolon marks the end of a line of code it already does what this colon is doing. Here’s the code rewritten to use semicolons (so Ruby won’t barf out errors)

      def convert_number_column_value(value)
        case value
          when FalseClass; 0
          when TrueClass;  1
          when '';         nil
          else value
        end
      end
which is really just the same as:
      def convert_number_column_value(value)
        case value
          when FalseClass then        0
          when TrueClass  then        1
          when ''         then        nil
          else value
        end
      end
and with newlines instead of “then”s:
      def convert_number_column_value(value)
        case value
          when FalseClass
           0
          when TrueClass
           1
          when ''
           nil
          else value
        end
      end
and
      def find_template_extension_for(template_path)
        if match = delegate_template_exists?(template_path)
          match.first.to_sym
        elsif erb_template_exists?(template_path);        :rhtml
        elsif builder_template_exists?(template_path);    :rxml
        elsif javascript_template_exists?(template_path); :rjs
        else
          raise ActionViewError, "No rhtml, rxml, rjs or delegate template found for #{template_path} in #{@base_path}" 
        end
      end
which is really:
      def find_template_extension_for(template_path)
        if match = delegate_template_exists?(template_path)
          match.first.to_sym
        elsif erb_template_exists?(template_path)        then     :rhtml
        elsif builder_template_exists?(template_path)    then     :rxml
        elsif javascript_template_exists?(template_path) then     :rjs
        else
          raise ActionViewError, "No rhtml, rxml, rjs or delegate template found for #{template_path} in #{@base_path}" 
        end
      end
and with newlines:
      def find_template_extension_for(template_path)
        if match = delegate_template_exists?(template_path)
          match.first.to_sym
        elsif erb_template_exists?(template_path) 
         :rhtml
        elsif builder_template_exists?(template_path)
         :rxml
        elsif javascript_template_exists?(template_path)
         :rjs
        else
          raise ActionViewError, "No rhtml, rxml, rjs or delegate template found for #{template_path} in #{@base_path}" 
        end
      end

As is often the case with Ruby/Rails this is largely an aesthetic issue. You can see some discussion about it here: http://www.ruby-forum.com/topic/75349

I’m curious what you think, are the colons pretty enough to stay or should the Rails code be adopted?

Edit: Ryan’s right, I’ve been making some alterations to this code while the post was live – my apologies for the underhandedness. Also I’m totally sorry for my spazzy comments. Even if it gives you some weird error it probably saved just fine. I’m working on un-jacking it.

Edit: If you’re running some form of *x system (macs included) and you’re getting annoyed by the warning output theres a simple way to hide everything but the regular ruby output:
rake test:units 2> /dev/null 

→ 9 comments Tags:

Rails: Custom 404 Pages

August 05, 2006 · 4 comments

I launched JCFootballProspects.com a while ago and things have been going smoothly but the site is stuck with the nasty black-on-white error messages when somebody types in a wrong address.

I’m now at the point where I want to give out some fancy 404 pages and I’ve been looking around at what other folks have done. I’m amazed to find that all the examples I can find have a 404.html page in the `#{RAILS_ROOT}/public` directory. I’m sure it’s effective to just have a plain-html file that gets served up for errors (and it’s certainly a lot better than the default ugly stuff) but I’m convinced there are advantages to having application-provided 404 pages as well.

There are three different kinds of errors I trap and respond to: * A user gives incorrect or insufficient parameters to a page view * A user attempts to access an action that doesn’t exist * A user attempts to access a controller that doesn’t exist

The first is easily handled – even the AWDWR book shows in an early example that you can output a flash message if something goes wrong with a request. The second is only slightly more complicated but, luckily, Ruby has a great way to respond to missing actions on controllers using method_missing. The solution to the third is provided by Rails’ routing capabilities.

How to hook up an app-driven 404 page: ==== The first thing I did was added a view to my default controller (mine is called ‘home’). I created ./app/views/home/404.rhtml and put just some basic stuff in it:
<%= content_tag 'h2', 'Whoops!' %>

<%= content_tag 'h3', 'Page not found' %>

[insert message here]

The next step was to add method_missing to my application controller:
1
2
3
4
5
6
7
8
9
10
11
class ApplicationController < ActionController::Base

  ...

  def method_missing(methodname, *args)
   @methodname = methodname
   @args = args
   render 'home/404', :status => 404
  end

end
And one super low priority route (put it at the very bottom) finishes off the job:
  map.error ':controllername',  :controller => 'home',
                              :action => '404'

→ 4 comments Tags:

Ruby: Parsing CSV files quickly

August 03, 2006 · 6 comments

I seem to have a limitless stream of excel files coming at me from clients. Most of them are of the same format: first line is the column name and the rest of the lines are data.

Ruby has excellent baked-in capabilities for handling files, data, and even CSV stuff. Still, it was pretty hard for me to figure out how to turn a CSV file into an array of hashes where each cell was named with the correct column name.

So here y’are folks: an easy way to turn CSV files into an array of hashes.

def csv_to_array(file_location)
  csv = CSV::parse(File.open(file_location, 'r') {|f| f.read })
  fields = csv.shift
  csv.collect { |record| Hash[*(0..(fields.length - 1)).collect {|index| [fields[index],record[index].to_s] }.flatten ] }
end

→ 6 comments Tags:

Rails Socket Config (Can't connect to local MySQL server through socket)

July 31, 2006 · 2 comments

(Note: this article might be better named “the most complicated database.yml file possible”)

Have you ever accidentally added your database.yml file to subversion or moved a database.yml file to a computer where it doesn’t belong? How about moving your dev server from your laptop to your desktop to your school’s web host to some new install of Suse you’re playing with?

Most of the time this is a big headache. You get errors like the one in the title where the socket was not found. Different flavors of *nix will put the default mysql socket in different places on the machine and it can be a pain to try to keep up with things.

Well, here’s a a database.yml file that can relieve you of ever having to remember which machine you’re on. Many thanks to LazyAtom go gave me the idea for this (who, in turn, got the idea from James Duncan Davidson

Behold – The Code:

login: &login
  adapter:  mysql
  username: user
  password: 
  socket:  <%=
    ['/opt/local/var/run/mysql5/mysqld.sock', # darwinports
     '/opt/local/var/run/mysqld/mysqld.sock', # darwinports, again
     '/var/run/mysqld/mysqld.sock',           # ubuntu/debian
     '/tmp/mysql.sock'].select { |f| File.exist? f }.first %>

development:
  <<: *login
  database: rails_development

test:
  <<: *login
  database: rails_test

production:
  <<: *login
  database: rails

Ta-Da. I just installed this on my dev box and things are working great already.

For those of you who run a staging server to test your app in production, you might want to check out the following setup (I’m seriously considering a staging server setup for my apps. Also, this is good if you’re absolutely dead-set on using the same database.yml file for multiple hosts and multiple configurations (just as long as you know the hostname).

<% host = `uname -n`.strip %>
user: &user
  <% if host.eql? 'staging' %>
  username: staging_user
  password: staging_pass
  <% else %>
  username: user
  password: pass
  <% end %>

login: &login
  <<: *user
  adapter:  mysql
  socket:  <%=
    ['/opt/local/var/run/mysql5/mysqld.sock', # darwinports
     '/opt/local/var/run/mysqld/mysqld.sock', # darwinports, again
     '/var/run/mysqld/mysqld.sock',           # ubuntu/debian
     '/tmp/mysql.sock']
     .select { |f| File.exist? f }.first %>

development:
  <<: *login
  database: rails_development

test:
  <<: *login
  database: rails_test

production:
  <<: *login
  database: rails

If you want to partition the login info for several different users on a large collaborative project you may want to have a special file (ignored by SVN) that holds just the login info for a given user:

login: &login
  adapter:  mysql
  username: user
  password: 
  socket:  <%=
    ['/opt/local/var/run/mysql5/mysqld.sock', # darwinports
     '/opt/local/var/run/mysqld/mysqld.sock', # darwinports, again
     '/var/run/mysqld/mysqld.sock',           # ubuntu/debian
     '/tmp/mysql.sock'].select { |f| File.exist? f }.first %>

# replace the above login details with information from ./config/dblogin.yml
<%= file = File.join(RAILS_ROOT, "config", "dblogin.yml")
    IO.read(file) if File.exist?(file) %>

development:
  <<: *login
  database: rails_development

test:
  <<: *login
  database: rails_test

production:
  <<: *login
  database: rails

→ 2 comments Tags:

Rails Integration Testing - How to learn

July 30, 2006 · 0 comments

I decided to try filling up my ./test/integration folder today because it was empty and I felt I must have been missing out. I can’t stand the idea that all the other Rails devs are enjoying some feature I haven’t found out about yet.

Integration tests seem pretty handy because functional tests are pretty basic – effectively unit tests for controllers rather than tests of real interactivity. So I figured I’d google around a bit to discover this integration testing thing and this is what I found:

(which currently doesn’t have a web server so I had to grab the page from Google’s cache).

That’s it. Those three meager pages are all the info I could find on Rails integration testing. In fact, I have reason to believe that the Rails Recipes book just re-used the code from Jamis Buck’s post – so even they don’t have any more for us.

Well, that’s not acceptable. I’m trying to follow a particular user’s experience through my app and I want to get this working without having to resort to SeleniumOnRails or some other browser-emulator.

My code so far

I’ve got a particular user type `Athlete` that logs in and checks their profile. They update a couple pieces of information and we check that the info was saved. Should be easy. (note: to understand this code check out Jamis’s article – you might need to look in the Google Cache for it)

class AthleteSessionTest < ActionController::IntegrationTest

fixtures  :users,
          :athlete_contacts,
          :coach_contacts,
          :junior_colleges,
          :combines
def test_randy_visits
  randy = new_session_as('Cliffton Player')
  randy.goes_to_profile_edit
  randy.updates_profile_with :first => 'Randy', :last => 'BadAsserson'
  randy.
end
private
module AthleteSessionDSL
def goes_to(place)
  get place
end
def goes_to_login
  get '/auth/login'
  assert_response :success
  assert_template 'auth/login'
end
def goes_to_profile_edit
  get '/athletes/profile_edit'
  assert_response :success
  assert_template 'athletes/profile_edit'
end
def logs_in_as(athlete)
  @current_user = users(athlete)
  post 'auth/login', :user => {:email => @current_user.email, :password => @current_user.password }
  assert_response :redirect
  follow_redirect!
  assert_template 'athletes/profile'
end
def updates_profile_with(contact)
  post 'athletes/update_contact', :contact => contact,
                                  :athlete_id => @current_user.id
  assert_flash_equal 'This profile has been updated with new information', :success
  assert_response :redirect
  follow_redirect!
  assert_template 'athletes/profile'
  contact.each { |k,v| assert_equal v, @current_user.profile.attributes[k.to_s] }
  end
end
end
def new_session
  open_session do |sess|
    sess.extend(AthleteSessionDSL)
    yield sess if block_given?
  end
end
def new_session_as(person)
  new_session do |sess|
    sess.goes_to_login
    sess.logs_in_as(person)
    yield sess if block_given?
  end
end
end

The Problem

So this is fine. So far it’s working great. The problem is that I’ve set it up so that everything my user does is part of my AthleteSessionDSL module in it’s own method. Everything `randy` does has to be a method within the `AthleteSessionDSL` module.

I wanted to try some non-modular navigation to give Randy a bit more of a free spirit. I thought I’d see if I could take some of the functionality out of the module and do it directly:

  def test_randy_visits
    randy = new_session_as('Cliffton Player')
    randy.goes_to_profile_edit
    randy.updates_profile_with :first => 'Randy', :last => 'BadAsserson'
    # trying some direct session navigation outside of the randy session object
    get 'athletes/profile'
    assert_response :success
    assert_template 'athletes/profile'
  end

What happened? Well, since `randy` is really just an instance of a Rails session with some handy methods attached to it I immediately left the session when I stopped using `randy`. My request to get the “athletes/profile” page resulted in a redirect to my login page because that method is authentication protected.

Well, what’s the solution? Do I have to make a million methods for everything I want Randy to do?

The Solution

It turns out to be quite simple. The methods we have been using like `assert_response` and `get` (and all the rest) are, in fact, methods of the session. The `open_session` call in the `new_session` module creates a session object with all the standard methods on it.

So not only can we group functionality into methods and roll those into included modules – we can also call things directly:

  def test_randy_visits
    randy = new_session_as('Cliffton Player')
    randy.goes_to_profile_edit
    randy.updates_profile_with :first => 'Randy', :last => 'BadAsserson'
    randy.get '/athletes/profile'
    randy.assert_response :success
    randy.assert_template 'athletes/profile'
  end

→ 0 comments Tags:

States with abbreviations

June 12, 2006 · 2 comments

Sometimes it’s hard to come by this list when you need it. Hope this saves somebody some time. Here are the 50 states with their two-letter abbreviations in a Ruby-formatted array.
1
2
3
4
5
6
7
8
9
10
11
[ ["ALABAMA","AL"],
  ["ALASKA","AK"],
  ["AMERICAN SAMOA","AS"],
  ["ARIZONA ","AZ"],
  ["ARKANSAS","AR"],
  ["CALIFORNIA ","CA"],
  ["COLORADO ","CO"],
  ["CONNECTICUT","CT"],
  ["DELAWARE","DE"],
  ["DISTRICT OF COLUMBIA","DC"],
  ["FEDERATED STATES OF MICRONESIA","FM"],
  ["FLORIDA","FL"],
  ["GEORGIA","GA"],
  ["GUAM","GU"],
  ["HAWAII","HI"],
  ["IDAHO","ID"],
  ["ILLINOIS","IL"],
  ["INDIANA","IN"],
  ["IOWA","IA"],
  ["KANSAS","KS"],
  ["KENTUCKY","KY"],
  ["LOUISIANA","LA"],
  ["MAINE","ME"],
  ["MARSHALL ISLANDS","MH"],
  ["MARYLAND","MD"],
  ["MASSACHUSETTS","MA"],
  ["MICHIGAN","MI"],
  ["MINNESOTA","MN"],
  ["MISSISSIPPI","MS"],
  ["MISSOURI","MO"],
  ["MONTANA","MT"],
  ["NEBRASKA","NE"],
  ["NEVADA","NV"],
  ["NEW HAMPSHIRE","NH"],
  ["NEW JERSEY","NJ"],
  ["NEW MEXICO","NM"],
  ["NEW YORK","NY"],
  ["NORTH CAROLINA","NC"],
  ["NORTH DAKOTA","ND"],
  ["NORTHERN MARIANA ISLANDS","MP"],
  ["OHIO","OH"],
  ["OKLAHOMA","OK"],
  ["OREGON","OR"],
  ["PALAU","PW"],
  ["PENNSYLVANIA","PA"],
  ["PUERTO RICO","PR"],
  ["RHODE ISLAND","RI"],
  ["SOUTH CAROLINA","SC"],
  ["SOUTH DAKOTA","SD"],
  ["TENNESSEE","TN"],
  ["TEXAS","TX"],
  ["UTAH","UT"],
  ["VERMONT","VT"],
  ["VIRGIN ISLANDS","VI"],
  ["VIRGINIA ","VA"],
  ["WASHINGTON","WA"],
  ["WEST VIRGINIA","WV"],
  ["WISCONSIN","WI"],
  ["WYOMING","WY"] ]

→ 2 comments Tags: