Testing Rails with Rack::Test
The biggest news in Rails 2.3 is its support for Rack, the WSGI inspired Ruby web server interface. Of all the Rack goodness that has come along lately, the one that has me the most excited is Bryan Helmkamp's Rack::Test library, of which Bryan said "Basically, I extracted Merb’s request helper code into a small, reusable, framework agnostic library."
I loved Merb's request specs. I suspect that I'm going to like Rack::Test too.
Installing Rack::Test
We'll start by installing Rack::Test and loading it into our Rails app. Append this line to config/environments/test.rb
:
config.gem "rack-test", :lib => "rack/test"
You can now use rake
to install it:
$ sudo rake gems:install RAILS_ENV=test
Running tests with "rake test:rack"
We want to be able to run these tests easily with rake, just like our unit or functional tests. Create a new file called lib/tasks/rack-test.task
and paste this code (courtesy of recipe 53 from the Advanced Rails Recipes book) into it:
namespace :test do
Rake::TestTask.new(:rack => "db:test:prepare") do |t|
t.libs << "test"
t.pattern = "test/rack/**/*_test.rb"
t.verbose = true
end
Rake::Task["test:rack"].comment = "Run the Rack::Test tests in test/rack"
end
task :test do
Rake::Task["test:rack"].invoke
end
Note that we have also extended the :test
task so that our Rack tests will get run after our unit and functional tests.
You should now be able to run rake test:rack
, but it won't do anything yet. Let's test it by dropping this code into a file called test/rack/dummy_test.rb
:
require File.join(File.dirname(__FILE__), *%w[.. test_helper])
class DummyTest < ActiveSupport::TestCase
include Rack::Test::Methods
def app
lambda { |env| [200, {}, "Coolness"] }
end
test "should be hooked up properly" do
get "/"
assert last_response.body.include?("Cool")
end
end
When you run rake test:rack
you should get something like this:
$ rake test:rack
(in /Users/graham/data/effectif/projects/canvas)
/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby -I"lib:test" "/Library/Ruby/Gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader.rb" "test/rack/login_test.rb"
Loaded suite /Library/Ruby/Gems/1.8/gems/rake-0.8.7/lib/rake/rake_test_loader
Started
.
Finished in 0.008904 seconds.
1 tests, 1 assertions, 0 failures, 0 errors
Testing your Rails app
You'll have noticed that the app
method (which is supposed to return an instance of the Rack application under test) doesn't actually have anything to do with Rails. It returns a callable object that needs to be compliant with the Rack API (apps are invoked via their call
method, which is also how you invoke a lambda).
Let's fix that by making the app
method return an instance of our Rails application.
For an example of how Rails instantiates a rack app have a look inside commands/server.rb. Scroll down until you find the call to Rack::Builder.new
. From there we can see that Rails is using ActionController::Dispatcher.new
. That should be enough for most purposes (if not you can see what else you may need to add by poking around in server.rb
).
That gives us a template test file that looks something like this:
require File.join(File.dirname(__FILE__), *%w[.. test_helper])
class MyTest < ActiveSupport::TestCase
include Rack::Test::Methods
def app
ActionController::Dispatcher.new
end
test "home page" do
get "/"
assert last_response.ok?
end
end
If you want to get a bit more fancy and test things that aren't handled by ActionController::Dispatcher
you can put your app together with Rack::Builder
. As a contrived example, if you wanted to test for the presence of static files (I said it was contrived) you could create your app like this:
def app
Rack::Builder.new {
map "/" do
use Rails::Rack::Static
run ActionController::Dispatcher.new
end
}.to_app
end
This article was written using Rails 2.3.2, Rack 1.0.0 and Rack::Test 0.3.0.
Why Bother?
Shortly after I first posted this article, crispee commented on reddit asking why you'd bother to use Rack::Test instead of the functional tests that come with Rails. It's a good question, and I completely failed to cover it. Ooops.
Firstly, Rack::Test is more similar to the Rails integration tests than to the functional tests. Functional tests only allow you test the actions and views of a single controller. Rack::Test allows you to write tests that visit pages anywhere on your site -- you can simulate the behaviour of a real browser to ensure that your entire app hangs together properly.
Secondly, both the Rails functional and integration tests are closely coupled to Rails. Imagine for a moment that you've been running a Rails app for a while now. You've invested a lot of effort in your application; users can log in and do all sorts of clever stuff. In the meantime you've been hand crafting the sales pages (e.g. home page, tour, FAQ, etc.) have added them to your app. All these static pages are served by one big controller and your routes file is getting to be a bit of a mess. It's high time that you introduced a CMS.
You've also recently stumbled across a hot new rack compatible web framework (for the sake of argument, let's call it Sinatra). You quite fancy using a simple Ruby CMS (sorry, shameless plug) while continuing to develop the main part of your application in Rails. Rack makes this very easy to do in the same Ruby process (see Rails meets Sinatra for details).
Obviously, you wrote a test to demonstrate that a new user can click on the "Buy Now" link on your home page, navigate their way through your signup forms, and end up on the home page. If you were also astute enough to write that test using Rack::Test, that test will still work after you move some of those pages into the Sinatra CMS.
If you refer to Pratik's "Rails meets Sinatra" post that I linked to above, you'll see that you'd only have to change the way the rack app is instantiated in your test:
def app
Rack::Builder.new {
# URLs starting with /account (logged in users) go to Rails
map "/account" do
run ActionController::Dispatcher.new
end
# Everything else goes to your CMS
map "/" do
run Sinatra.application
end
}.to_app
end
How cool is that?
Useful resources
For more information check out:
- The Sinatra documentation -- includes some handy pointers on using Rack::Test.
- The Scaling Rails screencast on Rack -- a terrific introduction to Rack, Metal and the Rails Rack middleware.