NOTE: You might have been redirected here. Don't worry! I've moved my active blog to 40. (with egg)

We faced and common problem on our project recently: we wanted to deploy two public-facing versions of our Ruby on Rails application at the same time -- demo version and a production version. There was a catch, of course: the demo and production deployments needed to expose different functionality to users. We used routes.rb to solve this, but found unit testing the route changes difficult until we discovered the with_routing method in the Rails test framework.

The Details:

Let's say you have a whole lot of functionality that is not ready for prime-time, but you still want to expose a password-protected (demo) version of this application to our client. But, since you paid so much for that domain name, you’d love it if people could go to the production site and see the “Coming Soon!”, “About Us” and “Info” sections. As developers, we certainly don’t want to create two versions of the application, or sprinkle environment/configuration checking code all over the app in an effort to keep certain sections locked-down in “production” mode, while open in “demo” mode.

One solution in Ruby on Rails is to add mappings to routes.rb that check the ENV['RAILS_ENV'], then route traffic appropriately depending on the currently running RAILS_ENV. As good test driven developers, we’ll test-drive these changes.

Oh, and before you ding me for the DRY principle, hold your horses… we’ll address that towards the end.

Demo – Everything is exposed (but password-protected): Production: Only “Coming Soon!”, “About Us” and “Info” is allowed:

require File.dirname(__FILE__) + '/../test_helper'

class RoutesTest
  def test_demo_routes
    opts ="{:controller"> "movie", :action => "show", :id => "1"}
    assert_routing "movie/show/1", opts, "should have normal routing"
  end

  def test_production_routes
    ENV['RAILS_ENV'] = 'production'
    contact_route = {:controller => "about_us", :action => "contact"}
    assert_routing "about_us/contact", contact_route, "contact is allowed"

    sign_up_route = {:controller => "info", :action => "sign_up"}
    assert_routing "info/sign_up", sign_up_route, "sign up is allowed"

    path = "movie/show/1"
    coming_soon_route = {:anything => path.split("/"), :controller => 'temp', :action => 'coming_soon'}
    assert_routing(path, coming_soon_route, "everything else goes to coming_soon")
  end
end

test_demo_routes passes since it uses the default generated routes, but test_production_routes fails as expected: we haven’t written the code that treats production differently yet. Let’s edit routes.rb next.

routes.rb (just the goodies, leaving out the comments and block)

map.connect 'info/:action', :controller => 'info'
map.connect 'about_us/:action', :controller => 'about_us'

map.connect ':controller/:action/:id' unless ENV['RAILS_ENV'] == 'production'

map.connect '*anything', :controller => 'temp', :action => "coming_soon"

Now we run our tests again and guess what? test_production_routes still fails. Why? Because routes.rb is loaded once at system startup and is not reevaluated; in other words, the routes are built once and only once, based on the RAILS_ENV at startup time. And since our RAILS_ENV was ‘test’ when we launched our tests, ENV['RAILS_ENV'] == 'production' evaluated to false, and thus the default, wide-open route is in effect for test_production_routes. So, how do we test this stuff?

with_routing, and no, that’s not a typo: with_routing is a method in the Rails testing framework that allows you to override the routes build at system startup. At the time of this writing, the only mention of with_routing in the entire Googleverse is its own sparse documentation, so I’m not sure how many people use it. Here’s how it works:

  with_routing do |map|
    map.draw do 
      #add code here that would
   #normally go in routes.rb
      map.connect 'some_controller/:action/:id'
    end
    #assert some stuff while still in the with_routing block
    assert_routing(x,y, “this applies while in with_routing”)
  end

Let’s edit test_production_routes:

    def test_production_routes1
      ENV['RAILS_ENV'] = 'production'
      with_routing do |map|
          map.draw do 
            map.connect 'info/:action', :controller => 'info'
        map.connect 'about_us/:action', :controller => 'about_us'

        map.connect ':controller/:action/:id' unless ENV['RAILS_ENV'] == 'production'

        map.connect '*anything', :controller => 'temp', :action => "coming_soon"
      end

    contact_route = {:controller => "about_us", :action => "contact"}

    assert_routing "about_us/contact", contact_route, "contact is allowed"

    sign_up_route = {:controller => "info", :action => "sign_up"}
    assert_routing "info/sign_up", sign_up_route, "sign up is allowed"

    path = "movie/show/1" coming_soon_route = {:anything => path.split("/"), :controller => 'temp', :action => 'coming_soon'}
    assert_routing(path, coming_soon_route, "everything else goes to coming_soon")
  end 
end

It’s kind of ugly, but I wanted to add enough detail to show how it works. Wrapping all of the routing and asserts in with_routing allows us to change the RAILS_ENV within the test, and the routes will obey the change.

Now just copy and paste those routes within with_routing into routes.rb… and keep them both in synch every time you change them… bleh, I don’t think so. That violates DRY (Don’t Repeat Yourself) for no good reason. What we really need to do is be able to reference the routes inside routes.rb whenever we want to. Let’s wrap them in a Class and method for easy referencing.

(Note: I’m not the best ruby programmer, so there might be a way of doing this that is more ruby-friendly, but since I’m coming into this after years of Java my first instinct is to wrap stuff in methods.)

routes.rb:

class RoutesLoader
def self.build_routes(map)
  map.connect 'info/:action'
  map.connect 'about_us/:action'

  map.connect ':controller/:action/:id'
  unless ENV['RAILS_ENV'] == 'production'

  map.connect '*anything', :controller => 'temp', :action => "coming_soon"
end
end

ActionController::Routing::Routes.draw do |map|
    RoutesLoader.build_routes(map)
end

This allows us to call RoutesLoader.build_routes(map) inside test_production_routes:

def test_production_routes
  ENV['RAILS_ENV'] = 'production'
  with_routing do |map|
      map.draw do 
        RoutesLoader.build_routes(map)
      end
    contact_route = {:controller => "about_us", :action => "contact"}
    …..

I hope this at least gives a bit more documentation for with_routing, and perhaps one more example for using routes.rb. Comments are welcome!


1 Comments (from old blog):

At 12/15/2006 7:26 AM, Rich said…

Thanks for the info. I've got an issue in a related area, and I've been scouring the net and documentation for answers but to no avail. Please help me out if you can!

I'm making a wiki based site, and after the www.mysite.com domain I wanted to have a wiki name before the controller/action/etc... stuff.

So I made an entry in the routes.rb file like this...

 map.connect ':wiki/:controller/:action/:id'

This works a treat when using the app, but when I try to write a functional test for my log in page like this...

 options = {:wiki => "acme", :controller => "account", :action => "log_in"}
  assert_routing "acme/account/log_in", options

  post :log_in, {:wiki => "acme", :controller => "account", :username=>"me", :password=>"me"}

... the assert_routing works, but the post to login comes back with an Exception saying "Need controller and action!"

Any ideas? Cheers, Rich.

Leave a Reply