Make your tests run 25% faster


By turning off the use_instantiated_fixtures feature.

But why?

Because they’re slow! Didn’t you read the comment in your test_helper.rb? Yes, this one:

# Instantiated fixtures are slow, but give you @david where otherwise you 
# would need people(:david).  If you don't want to migrate your existing 
# test cases which use the @david style and don't mind the speed hit (each 
# instantiated fixtures translates to a database query per test method), 
# then set this back to true.

But what about those nice @david variables?

They must be gone. Sorry. The tests are slow because of them. The above comment explains why.

On my current project, the user_test.rb (which is a real monster, sporting 88 kb of code) took about 20 seconds to run before the conversion and only 11 seconds after. This file loads 21 fixture files. A little bit of grepping tells me that this makes 103 objects to instantiate before each of the 195 test methods, which in turn means 20,085 queries to database. After the conversion, fixtures are only instantiated when necessary, which probably means 2 to 5 queries per test method. I suggest that you make similar calculations on some of tests in your project.

But is it really 25%?

Well, that depends on your project specifics and coding style. On my current project, the time to run all the tests dropped from about 150 seconds to under 110 seconds. It’s almost 27% by my counting.

But it’s really inconvenient to write all those people(:david) calls!

Well, you can assign the fixture to some local variable and then use the variable.

But it would be a lot of work to convert my tests!

No, not at all. Just use this little script that I wrote:

require 'config/environment' 
require 'active_record/fixtures'  

for file_name in ARGV 
  old_file_name = file_name + '.bak' 
  File.rename file_name, old_file_name 
  puts "renamed #{file_name} to #{old_file_name}" 
  out =, 'w') 
  puts "writing #{file_name}"  

  fxs = {} 
  File.foreach(old_file_name) do |line| 
    if line =~ /fixtures ([\w:, \t]+)/ 
      out.puts line 
      tables = $':','').split(', ') 
      fx = Fixtures::create_fixtures('test/fixtures', tables) 
      fx = [fx] unless tables.size > 1 
      fx.each do |fs| 
        fs.each do |f| 
          name = f.first 
          cls = f.last.instance_variable_get('@class_name').tableize 
          fxs["@#{name}"] = "#{cls}(:#{name})" 
      out.puts line.gsub(/@\w+/){|s| fxs[s] || s} 

Copy the above code and save it as a file in script directory, e.g. script/fxcvt.rb. Then you can run it as:

ruby script/fxcvt.rb test/unit/user_test.rb

to convert one file or:

ruby script/fxcvt.rb test/*/*.rb

to convert all of them at once.

The script takes a list of test filenames on command line. For each file it saves a backup copy (beware: running it second time on the same file will overwrite old backup (but that’s no problem since you use SVN or CVS, don’t you?)) and writes the converted file under original name. If it finds fixtures line, it loads the declared fixtures and then it fills fxs hash with short and long names of fixtures. On all other lines it looks for anything that starts with @ sign. If there is corresponding entry in fxs hash, it is converted to appropriate fixture call (i.e. @david to people(:david)).

When you convert all the tests, turn off instantiated fixtures in test_helper.rb:

self.use_instantiated_fixtures = false

and try to run your tests. Some of them will probably fail because of slight differences in semantics of both solutions. As I found out, most of these problems can be solved just by assigning the fixture to a local variable and using this variable instead of the fixture. So if you have a test looking like this:

def test_something 
	users(:one).login = 'something' 
	assert users(:one).login 

first try to change it to:

def test_something 
	u = users(:one) 
	u.login = 'something' 
	assert u.login 

This worked for me in 9 cases per 10.

Add to:
Digg! digg blog_head.png reddit delicioussmall.gif

Leave a Reply

Fill in your details below or click an icon to log in: Logo

You are commenting using your account. Log Out /  Change )

Google photo

You are commenting using your Google account. Log Out /  Change )

Twitter picture

You are commenting using your Twitter account. Log Out /  Change )

Facebook photo

You are commenting using your Facebook account. Log Out /  Change )

Connecting to %s

%d bloggers like this: