Quick Ruby Tests with Bash
by Nick Gauthier on
In Ruby on Rails development, we have great gems like Guard that will re-run tests or other tasks based on changing files. I was interested in finding something more lightweight but less configurable and flexible that I could use on smaller projects.
I ended up writing this quick bash script that I put on my path called live
:
#/usr/bin/env sh # Usage "live command" clear $* while inotifywait -qr -e close_write *; do clear; $*; done
This script takes a command as an argument and re-runs the command whenever any file in the current directory changes. So, you can simply re-run the tests of a project by running live rake
. Or, you could re-output a project directory with live tree
. It’s like watch
except evented on write changes.
The next thing I noticed while using this on larger (rails) projects is that project boot is slow and the tests run in different groups that each boot the environment.
Note the raw time it takes to run the project’s tests:
$ time rake Loaded suite /home/nick/workspace/giftsmart/.bundle/gems/ruby/1.9.1/gems/rake-0.9.2.2/lib/rake/rake_test_loader Started .... Finished in 0.588015 seconds. 4 tests, 30 assertions, 0 failures, 0 errors, 0 skips Test run options: --seed 54473 Loaded suite /home/nick/workspace/giftsmart/.bundle/gems/ruby/1.9.1/gems/rake-0.9.2.2/lib/rake/rake_test_loader Started ............... Finished in 3.679384 seconds. 15 tests, 77 assertions, 0 failures, 0 errors, 0 skips Test run options: --seed 45532 real 0m41.495s user 0m38.502s sys 0m1.928s
Seriously?! 41.5 seconds to run just over 4 seconds worth of tests? WAT? And this is using ruby-1.9.3-falcon!
So, my first step was to merge the environments and optionally skip db:reset
:
# lib/tasks/testing.rake namespace :test do desc 'Run tests quickly by merging all types and not resetting db' Rake::TestTask.new('fast') do |t| t.libs << 'test' t.pattern = "test/**/*_test.rb" end namespace :fast do desc 'Run tests quickly, but also reset db' task :db => ['db:test:prepare', 'test:fast'] end end
This provides rake test:fast:db
which reset the db and runs the tasks merged as one, and rake test:fast
which merges the tasks and doesn’t reset the db. Here’s the result:
$ time rake test:fast:db Loaded suite /home/nick/workspace/giftsmart/.bundle/gems/ruby/1.9.1/gems/rake-0.9.2.2/lib/rake/rake_test_loader Started ................... Finished in 3.752755 seconds. 19 tests, 107 assertions, 0 failures, 0 errors, 0 skips Test run options: --seed 57655 real 0m28.430s user 0m26.138s sys 0m1.216s
OK, that’s a 46% improvement. Now, without the db:
$ time rake test:fast Loaded suite /home/nick/workspace/giftsmart/.bundle/gems/ruby/1.9.1/gems/rake-0.9.2.2/lib/rake/rake_test_loader Started ................... Finished in 3.790390 seconds. 19 tests, 107 assertions, 0 failures, 0 errors, 0 skips Test run options: --seed 53405 real 0m21.623s user 0m20.525s sys 0m1.020s
Better, a 92% improvement! But it’s still 3.8 seconds worth of test at 21.6 seconds. Lame.
Now it’s time for drastic measures. When I’m running tests, I see this process:
/usr/bin/ruby1.9.1 -Ilib:test /path/to/my/project/.bundle/gems/ruby/1.9.1/gems/rake-0.9.2.2/lib/rake/rake_test_loader.rb test/unit/**/*_test.rb
This is because Rake::TestTask shells out to ruby to keep the environment clean. This works great for projects with minimal boot times. But in my case, I have a much larger Rails boot I have to worry about. This means I’m still booting Rails twice!
So, I made a short ruby script that gives me direct access to the rake test loader for the current project:
#!/usr/bin/env sh ruby -Ilib:test `bundle list rake`/lib/rake/rake_test_loader.rb $*
Now, here are my results:
$ time rtest test/**/*_test.rb Run options: # Running tests: ................... Finished tests in 3.070074s, 6.1888 tests/s, 34.8526 assertions/s. 19 tests, 107 assertions, 0 failures, 0 errors, 0 skips real 0m9.579s user 0m9.089s sys 0m0.324s
There we go! That’s 333% faster! Now the total time to run my tests is 1xRails and 1xTest. This is probably the minimal boot time I could get without keeping the environment hot loaded. The best part is, I can combine my two scripts:
$ live time rtest test/**/*_test.rb
Now I have a very fast and minimal live-updating setup written in 4 lines of bash that is portable across projects.
blog comments powered by Disqus