Database-less Testing

Posted by Pat Tue, 24 Jan 2006 10:08:00 GMT

Rails is awesome for building database-driven applications, and has a very nice testing framework built right in. Unfortunately it’s a bit too built in. I’ve recently been writing a number of small applications that don’t use a database. I started off writing my tests as usual..then rake blew up on me. I honestly don’t remember the exact errors…I asked on the Rails list, and made a frustrated post after my first question didn’t lead to satisfactory results. No luck still.

Okay enough about that..I think I’ve found the answer. Basically you need to strip AR entirely out of your Rails project, and redefine one of the rake tasks.

First of all, in your app’s environment.rb file, make sure you don’t load AR:

config.frameworks -= [ :active_record ]

You should of course exclude any other frameworks you don’t want.

The next thing to do is make a stripped down version of the test_help.rb file. This isn’t the same as test_helper.rb which lives in the test/ dir, it’s a file named test_help.rb I found in the bowels of Railties, but without the AR and fixture loading stuff. This should go in the lib/ dir.

lib/test_help_without_ar.rb
require 'application'

# Make double-sure the RAILS_ENV is set to test, 
# so fixtures are loaded to the right database
silence_warnings { RAILS_ENV = "test" }

require 'test/unit'
require 'action_controller/test_process'
require 'action_web_service/test_invoke'
require 'breakpoint'

Here’s the original code in railties, so you can see what I took out.

require 'application'

# Make double-sure the RAILS_ENV is set to test, 
# so fixtures are loaded to the right database
silence_warnings { RAILS_ENV = "test" }

require 'test/unit'
require 'active_record/fixtures'
require 'action_controller/test_process'
require 'action_web_service/test_invoke'
require 'breakpoint'

Test::Unit::TestCase.fixture_path = RAILS_ROOT + "/test/fixtures/"

def create_fixtures(*table_names)
  Fixtures.create_fixtures(RAILS_ROOT + "/test/fixtures", table_names)
end

Now go back and edit test/test_helper.rb to use this test_help file instead of the default rails one. It’s a simple matter of loading our custom test_help file:

test/test_helper.rb
ENV["RAILS_ENV"] = "test"
require File.expand_path(File.dirname(__FILE__) + "/../config/environment")
require 'test_help_without_ar'

class Test::Unit::TestCase
  # Add whatever specific functionality you want
end

After some more digging through railties, I came across the prepare_test_database rake task. Redefine it to do nothing, and everything works fine. I found some code that Blair Zajac wrote to provide a method for redefining a rake task. I’m only showing the code here, but his full comments and credit appear in the attached file.

lib/tasks/testing.rake
# The following code is by Blair Zajac and appeared in the Ruby on Rails mailing list.
# See archived message here:  http://article.gmane.org/gmane.comp.lang.ruby.rails/27426

module Rake
   class Task
     # Clear all existing actions for the given task and then set the action for the task to the given block.
     def self.redefine_task(args, &block)
       task_name, deps = resolve_args(args)
       TASKS.delete(task_name.to_s)
       define_task(args, &block)
     end
   end
end

# Clear all existing actions for the given task and then set the action for the task to the given block.
def redefine_task(args, &block)
   Rake::Task.redefine_task(args, &block)
end

# The following code is by (obvious) master hacker Pat Maddox
desc "Prepare the test database"
redefine_task :prepare_test_database do |t|
  # Don't do anything, there's no database to prepare
end

Final Thoughts

This gave me a real headache back when I first tried to do it, I couldn’t find anything to help me out. However now I can happily write all my tests and use rake to automatically run the tests. I’ve noticed that it seems to be a bit slow..I’m really not sure why, but if it’s working then I’m happy. Hopefully someone else will find this useful.

Posted in  | Tags ,  | 6 comments

Comments

  1. Jim Weirich said about 5 hours later:

    Hmmm … I’m wondering if it is worth adding a delete_task and redefine_task to the base Rake system. Looks like there is a good user case for it.

  2. Eric Hodel said about 9 hours later:

    silence_warnings is such an ugly, ugly hack. Ruby provides defined? and String#replace.

  3. Pat said about 12 hours later:

    Hey Eric,

    I was a bit confused at first because I couldn’t remember using silence_warnings in my solution. It’s actually not a hack at all…I edited the blog post to show the original test_help.rb file in railties, and silence_warnings is included there. In my code, I simply got rid of the AR and fixture stuff I didn’t need.

    Just to try something out, I commented out the silence_warnings line in my updated test_help.rb, and everything runs fine still. However, I’d just like to point out that, unless you explicitly change it all your apps are using silence_warnings during testing by default.

    Pat

  4. indy said 21 days later:

    The redefine_task doesn’t work with rake-0.7.0, it gives the error:

    undefined method 'resolve_args' for Rake::Task:Class

    rake-0.6.2 seems be fine however

    Indy

  5. Jim Weirich said 22 days later:

    Rake 0.7.0 has done some significant internal reorganizations. In particular, the task list is now stored in a Rake application object, possibly allowing multiple rake dependency trees in a single application. Therefore the task management functions were take out of the Task class and moved to a TaskManager class. The following hack should provide the same functionality as the above code.

    module Rake
      module TaskManager
         # Clear all existing actions for the given task and then set the
         # action for the task to the given block.
         def redefine_task(task_class, args, &block)
           task_name, deps = resolve_args(args)
           @tasks.delete(task_name.to_s)
           define_task(task_class, args, &block)
         end
       end
    
       class Task
         def self.redefine_task(args, &block)
           Rake.application.redefine_task(self, args, &block)
         end
       end
    end
    
    # Clear all existing actions for the given task and then set the
    # action for the task to the given block.
    def redefine_task(args, &block)
       Rake::Task.redefine_task(args, &block)
    end
    

    I would point out that the redefine_task command will always redefine the named task to be a plain old vanilla Task, even if it was a FileTask or MultiTask previously.

  6. Jim Weirich said 22 days later:

    Just a note about the whole redefine thing: Hacks like this are playing around with some of the internals of Rake, and as such, it is not a publically supported API. There is no guarantee going forward that the internals will not change again as they did for 0.7.0. (I.e. my hack given above is just that, a hack with no guarantee of future support).

    Not that I mind this kind of stuff. In fact, it is hacks like this that help me determine where rake needs to go in the future.

    So, with that disclaimer in mind, have fun.

(leave url/email »)

   Preview comment