Kevin McKelvin

Faster Rails Testing with Zeus

21 March 2013

Rails boot times suck. Waiting between 5 and 25 seconds to restart a server, run a test or to open the Rails console just doesn’t cut it. When doing TDD, you need the shortest feedback cycle possible.

Performance patched Ruby installs give you a significant improvement, and with Ruby 2.0, the require path is much faster than it used to be. These improvements are great, but we can still do more.

On cue, in comes Zeus. In a nutshell, Zeus preloads your development and test environments and makes the on-demand initialization of Rails servers, tests, rake tasks and consoles blisteringly fast. More technically speaking, Zeus is a process checkpointer for single-threaded applications. It’s been built for Ruby, but support for other languages is planned. For this post however, I just care about Ruby.

Zeus is an external piece of software to your Rails app. It’s distributed as a gem, but must not be included in your Gemfile. It’s designed to be run outside of bundler.

Installing

To install Zeus, simply install the gem: gem install zeus

Once Zeus is installed, cd to your project directory and run: zeus start

Zeus then fires up Ruby and checkpoints it at a point where you can connect to the process to run commands such as rake, test, rspec, console, and server.

To demonstrate, once Zeus has initialised, run zeus rspec spec and watch the magic happen.

Vim integration

@r00k has a really nifty script in his vimrc that will run tests through Zeus. Just drop this into your .vimrc.

""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
" Test-running stuff
""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""""
function! RunCurrentTest()
  let in_test_file = match(expand("%"), '\(.feature\|_spec.rb\|_test.rb\)$') != -1
  if in_test_file
    call SetTestFile()

    if match(expand('%'), '\.feature$') != -1
      call SetTestRunner("!zeus cucumber")
      exec g:bjo_test_runner g:bjo_test_file
    elseif match(expand('%'), '_spec\.rb$') != -1
      call SetTestRunner("!zeus rspec")
      exec g:bjo_test_runner g:bjo_test_file
    else
      call SetTestRunner("!ruby -Itest")
      exec g:bjo_test_runner g:bjo_test_file
    endif
  else
    exec g:bjo_test_runner g:bjo_test_file
  endif
endfunction

function! SetTestRunner(runner)
  let g:bjo_test_runner=a:runner
endfunction

function! RunCurrentLineInTest()
  let in_test_file = match(expand("%"), '\(.feature\|_spec.rb\|_test.rb\)$') != -1
  if in_test_file
    call SetTestFileWithLine()
  end

  exec "!zeus rspec" g:bjo_test_file . ":" . g:bjo_test_file_line
endfunction

function! SetTestFile()
  let g:bjo_test_file=@%
endfunction

function! SetTestFileWithLine()
  let g:bjo_test_file=@%
  let g:bjo_test_file_line=line(".")
endfunction

Bind it to a hotkey:

map <leader>rt :call RunCurrentTest()<CR>
map <leader>rl :call RunCurrentLineInTest()<CR>

Gotcha with oh-my-zsh

If you’re using the OMZ bundler plugin, you’ll need to remove the zeus command from the plugin’s autobundle list in ~/.oh-my-zsh/plugins/bundler/bundler.plugin.zsh. Zeus will run your application through bundler by itself. Running Zeus inside bundler slows it down.

I find myself making single line changes and just running my specs habitually now, where I wouldn’t have bothered in the past. The responsiveness and feedback is addictive!

Happy testing!


Kevin McKelvin

These are the online musings of Kevin McKelvin. He is the CTO at Resource Guru, and lives in Newcastle upon Tyne, UK.