Test Drive mod_rewrite Rules With Test::Unit

I've generated redirects in Apache using mod_rewrite so many times, I can do it in my sleep. Unfortunately, I think I have done them in my sleep. The result? Time wasted chasing down unexpected application behavior and the wrath of the Viget development team.

When a recent re-launch brought with it a change to the Information Architecture of the site, I was determined not to repeat the mistakes of the past. Though this site isn't a Rails application, I wanted to use familiar tools to automate the verification of the rewrite rules I was about to create. In thinking about this a bit more, it was clearly a "behavior-driven" approach - I was specifying the behavior of the web server before its implementation.

Since I'm writing about the majority of my tests with Shoulda these days, I immediately gravitated toward its familiar DSL to come up with this:

should_redirect '/pocket_guides/', :to => '/live-healthy/pocket_guides/' 

I had two goals in mind with this approach: clarity and readability. To see how well I did, I shared the collection of tests I created with Kara to ensure that the redirects they represented were accurate. Within minutes, she had corrected a couple of errors I had introduced. My test suite was now complete.

Putting it Together

I wanted something that was easy to use, so I added the should_redirect macro to a subclass of Test::Unit::TestCase which I used in my actual tests:

class ChecRedirectTest < HTTPRedirectTest self.domain = 'chec.lab.viget.com' should_redirect '/pocket_guides/', :to => '/live-healthy/pocket_guides/' end 

From here, I was able to adopt a familiar behavior-driven approach. I wrote failing tests, and then generated the appropriate configuration within Apache to make the tests pass. When I broke a regression, I was quickly alerted to the fact with a familiar report:

Running Tests

Unintended Consequences

Getting the redirects correct isn't the only place that errors can creep in. To cover all cases, you need to have checks in place to ensure that URLs that are not supposed to redirect are behaving as expected. I added this ability using a similar syntax:

 should_not_redirect '/' 

With that guard in place, I was instantly alerted to problems with unexpected redirection:

No Redirection

Trying it Out

The implementation of the HTTPRedirectTest class is available as a Gist on Github. Go ahead and grab it, and start off by creating your own redirection test as a subclass:

require 'http_redirect_test' class ExampleRedirectTest < HTTPRedirectTest self.domain = 'development.example.com' # domain to test against should_not_redirect '/' # initial guard end 

All green? Continue adding tests. If you want to check redirection of subdirectories, you can do that as well:

 should_redirect '/blog/*', :to => '/*' 

When you're satisfied with the redirection rules, testing in production is just as easy. Just change the domain to point to your production server:

class ExampleRedirectTest < HTTPRedirectTest self.domain = 'production.example.com' # domain to test against should_not_redirect '/' # initial guard should_redirect '/blog/*', :to => '/*' end 

Bonus: Refactoring Rewrite Rules

Red, Green, ... Refactor? Once you've built up your regressions, you can start tweaking some rewrite rules without worrying about breaking others – your tests will back you up. These rules I created as part of my test plan can be condensed from this:

RewriteRule ^/resources/faq/?$ http://chec.lab.viget.com/live-healthy/faq/ [QSA,R=301,L] RewriteRule ^/resources/faq/(.+)$ http://chec.lab.viget.com/live-healthy/faq/$1 [QSA,R=301,L] 

Down to this:

 RewriteRule ^/resources/faq/?(.*)$ http://chec.lab.viget.com/live-healthy/faq/$1 [QSA,R=301,L] 

The behavior stays the same, but our rewrite rules are now easier to understand (as far as mod_rewrite rules go).

Additional Resources

Patrick is development director in Viget's Boulder, CO, office. He writes clean Ruby code and automates system infrastructure for clients such as Shure and Volunteers of America.

More posts by Patrick