autotest のさらなる自動化

autotest は,TDD や BDD をやる上では,かかせないツールになっています.通常,rake とタイプするだけで,テストをやってくれますが,すでに数千のテストを記述していると時間ばっかりかかってしまいます.autotest を導入すると修正が行われたファイルについてのテストのみが自動的に実行されるので,開発効率を上げることができます.

ただし,autotest を単体で実行すると以下のような問題点があります.

  1. Ruby 1.9 の場合,テストプロセスが起動する度に,コンパイルされるため,テストに時間がかかります
  2. spork を導入するとこの問題は回避されますが,config 以下等のファイルを更新した時には,spork プロセスを再起動しないといけません.この再起動を忘れて,テストがいつまでたっても成功しなくて悩んだことが多々ありました.
  3. ファイルを追加した時には,autotest は反応しないので,autotest に SIGINT を送る(^c を押す)必要があります.

このあたりを watchr を使って解決したので,ここにまとめておきます.

参考にしたサイトは以下の二つです.二つ目のサイトで watchr の仕組みがわかったので,今回,autotest 自体も同時に再起動することにしました.

.autotest の記述

ここでは,テストで無視するファイル一覧の記述と,実装ファイルとテストファイルの関係を書いています.今回は,rsec による BDD を行うので,.rb と _spec.rb のマッピングを記述しています.また,spec_helper.rb,spork.watchr,.autotest,config 以下のファイルは watchr の方で監視するので,ここでは無視します.

#require 'autotest/growl'
#require 'autotest/screen'
require 'autotest/timestamp'

Autotest.add_hook :initialize do |autotest|
  autotest.instance_eval do
    add_exception %r%^\./(?:db|tex|doc|log|public|script|tmp|vendor|spec/spec_helper.rb|spec/spork.watchr|config|\.autotest)%
  end
  autotest.add_mapping(/app\/([\/\w_]+)\.rb$/) do |f, matched|
    "spec/#{matched[1]}_spec.rb"
  end
end

spec_helper.rb の記述

spec_helper.rb の詳細はまた後日記載するので,関連する部分だけ書きます.基本は,spork 起動時のみ実行する部分と,毎回実行する部分を切り分けることです.起動時のみ実行する部分が増えるほどテスト処理は高速化します.

# -*- coding: utf-8 -*-
require 'rubygems'
require 'spork'

# ここに書かれた内容は spork 起動時にのみ実行される
Spork.prefork do
  # Loading more in this block will cause your tests to run faster. However,
  # if you change any configuration or code from libraries loaded here, you'll
  # need to restart spork for it take effect.

  # This file is copied to spec/ when you run 'rails generate rspec:install'
  ENV["RAILS_ENV"] ||= 'test'
  require File.expand_path("../../config/environment", __FILE__)
  require 'rspec/rails'

  # Requires supporting ruby files with custom matchers and macros, etc,
  # in spec/support/ and its subdirectories.
  Dir[Rails.root.join("spec/support/**/*.rb")].each {|f| require f}

  RSpec.configure do |config|
    # == Mock Framework
    #
    # If you prefer to use mocha, flexmock or RR, uncomment the appropriate line:
    #
    # config.mock_with :mocha
    # config.mock_with :flexmock
    # config.mock_with :rr
    config.mock_with :rspec

    # Remove this line if you're not using ActiveRecord or ActiveRecord fixtures
    config.fixture_path = "#{::Rails.root}/spec/fixtures"

    # If you're not using ActiveRecord, or you'd prefer not to run each of your
    # examples within a transaction, remove the following line or assign false
    # instead of true.
    config.use_transactional_fixtures = true

    config.global_fixtures = :users, :half_kikans, :campuses, :years, :admins
    ActiveSupport::Dependencies.clear
  end

  # mock_モデル名を用意しておく
  MockTables = %w(year gakunen campus (以下続くが省略)
  )

  MockTables.each do |k|
    modelClass = k.camelize
    eval("def mock_#{k}(stubs={}) @mock_#{k} ||= mock_model(#{modelClass}, stubs).as_null_object end")
  end

end

# こちらはテストの度に実行される
Spork.each_run do
end

.rspec の記述

このファイルに --drb を記述しておくことで,プロセスを新規に起動するのではなく,spork 側で処理を行うようになります.

--colour
--format progress
--drb

spork.watchr の記述

ここまでやれば,spork をバックグラウンドで実行後に,autotest をすれば 1 の問題は解決します.ただし,残りの 2, 3 の問題を解決するために,もう少し頑張ります.そのため,spork のプロセスを watchr で監視します.

# spork を起動します
$spork_pid = spawn('spork') unless $spork_pid
# 15 秒待ちます.単に待っていると時間が長く感じるので,カウントダウンしています.
15.times do |i|
  puts 15-i
  sleep 1
end
# autotest を起動します: -c (Red->Green の時に全テストを行わない),-f (起動時に全テストを行わない) 
$autotest_pid = spawn('autotest', '-c', '-f', '-s', 'rspec2') unless $autotest_pid
# config 以下,spec/spec_helper.rb,spec/spork.watchr を監視します.
watch('config/.*\.rb|spec/spec_helper.rb|spec/spork.watchr') do |m|
  # autotest のプロセスを殺します.
  puts "killing autotest... (pid:#{$autotest_pid})"
  Process.kill(9, $autotest_pid)
  # spork を殺して再起動します.
  puts "killing spork... (pid:#{$spork_pid})"
  Process.kill(2, $spork_pid)
  $spork_pid = spawn('spork')
  # 15 秒待ちます.単に待っていると時間が長く感じるので,カウントダウンしています.
  15.times do |i|
    puts 15-i
    sleep 1
  end
  # autotest を起動します
  $autotest_pid = spawn('autotest', '-c', '-f', '-s', 'rspec2')
end

.zshrc の記述

function alltest() { kill -2 `ps auxw | grep autotest | grep ruby | awk '{print $2}'` }
alias bes='be watchr spec/spork.watchr &'

実際の動作

実行
ターミナルで、bes とタイプします.
spec や 実装を書き換えた時
autotest の機能で書き換えたファイルに関する部分だけテストが実行されます.
controller を追加した時
この場合,基本的には config/routes.db も書き換えることが多いので,r g controller の後,*_routing_spec.rb も追加し,最後に routes.db を書き換えます.こうすると watchr が作動し,autotest の停止、spork の再起動,15 秒後に autotest の起動となります.なお,この15秒はシステムの規模に依存します.最近ファイルが増えてきて spork の起動に 14 秒かかるようになっているので,近々 20 秒に延長するつもりでいます.autotest が再起動したため,追加した controller は autotest の管理下に入ります.したがって,上書き保存をするだけでテストが起動します.
model を追加した時
この場合,spec/spec_helper.rb 内の mock を追加するので,同様に spork・autotest の再起動になります.
全テストを実行したい時
autotest も watchr によりバックグラウンド起動しているため,SIGINT を簡単に送れません.そこで,.zshrc に記載した alltest 関数を実行します.これは autotest のプロセスを探して、SIGINT を送っているだけです。

まとめ

この仕組みを導入した結果,autotest が完全自動になったため,キーボードをタイプする必要がなくなりました.そのため,現在は遊んでいるノート PC を開発機の隣に置いておき,ssh 経由で bes を実行しています.