autotest の導入方法

前々から使いたかったautotest を導入したので手順を記録します。OS はMac OS X(10.5.5), ruby は1.8.7です。
なお、参考にしたのは「まるごと Ruby! Vol.1(p.124~p.127)」です。

やること

  • autotest のインストール
  • aiutotest を使ってみる
  • autotest のカスタマイズ
    • Growl での通知
    • screen での通知

autotest とは

autotest とは、ruby で使える自動テストツールです。次のような特徴を持ってます。

  • ファイル監視

ファイルを監視してくれて、ファイルが変更されたら自動的にテストを走らせてくれます。自分でrake〜とか、ruby〜とか打ってテストを走らせなくていいってことですね。

  • 通知

また、通知という機能も見逃せません。テスト結果をメールで通知したり、Mac ユーザならGrowl を使った通知も簡単にできちゃうという寸法です。
こんな感じ。




つまり、ファイルを編集していれば、勝手にテストが走って勝手に結果も通知してくれるという、まさに世界が変わるツールなのです。

インストール

autotest はZenTest というgem に組み込まれているので、それをインストールします。これだけでおk。

$ sudo gem install ZenTest

使ってみる

ruby のテスティングフレームワークとしては、主に「Test::Unit」と「RSpec」がありますが、autotest はどちらとも連携できます。今回は、ruby が入っていれば使える「Test::Unit」を例に使ってみます。

ファイル作成

以下のようにディレクトリを作成

$ mkdir -p autotest_example/lib
$ mkdir -p autotest_example/test


さらに、テストファイルと、テスト対象となるファイルを作成します。

  • autotest_example/test/test_example.rb
require 'example'
require 'test/unit'

class TestExample < Test::Unit::TestCase
  def test_x
    assert_equal 1, Example.new.x  
  end
end
  • autotest_example/lib/example.rb
class Example
  def x
    # 最初はテストが落ちるように何も返さない
  end
end
autotest 実行

これで準備完了。autotest_example ディレクトリで次のコマンドを実行します。

$ autotest


すると、以下のような実行結果がでて、とまります。

/opt/local/bin/ruby -I.:lib:test -rtest/unit -e "%w[test/test_example.rb].each { |f| require f }" 
| unit_diff -u
Loaded suite -e
Started
F
Finished in 0.009735 seconds.

1) Failure:
test_x(TestExample) [./test/test_example.rb:6]:
--- /var/folders/NV/NVFCfIgDFDOS9YNDHnhFuk+++TI/-Tmp-/expect20080917-736-w0gmhe-0
2008-09-17 00:33:35.000000000 +0900
+++ /var/folders/NV/NVFCfIgDFDOS9YNDHnhFuk+++TI/-Tmp-/butwas20080917-736-1q7stc9-0
2008-09-17 00:33:35.000000000 +0900
@@ -1 +1 @@
-<1>
+<nil>

1 tests, 1 assertions, 1 failures, 0 errors

autotest はdiff 形式でテスト結果を表示するようになっています。



Column::autotest の規約について

ここで少し横道にそれて、autotest の規約について軽く触れます。autotest には以下のような規約があります。

  • テストファイルはtest ディレクトリ下に置く
  • テスト対象ファイルはlib ディレクトリ以下に置く
  • テスト対象ファイルを「example.rb」としたとき
    • テストファイル名は test_example.rb となる
    • テストクラス名は「TestExample」となる

先ほどautotest を実行したとき、最初は以下のようになっていました。

/opt/local/bin/ruby -I.:lib:test -rtest/unit -e "%w[test/test_example.rb].each { |f| require f }"


ここでは最初に、lib ディレクトリ、test ディレクトリをロードパスに加え、さらにtest/unit をrequire しています。
これによって、テストファイル内では「require "../lib/example"」のように書く必要がない。一方、「test/unit」のrequire もしなくていかと思ったら、変更を検知した際に走るテストでは「test/unit」をrequire しないというKY 実装になっているので、テストファイル内でrequire してやる必要がある。


ファイルを修正し、テストを通過させる

先程テストを走らせた際にはこけてとまってしまった。autotest は現在ファイルの変更を監視している状態にあるので、ファイルを変更して保存してみます。

  • autotest_example/lib/example.rb
class Example
  def x
    1
  end
end


すると、保存した瞬間にテストが走るはずです。結果は以下のように表示されます。

/opt/local/bin/ruby -I.:lib:test test/test_example.rb -n "/^(test_x)$/" | unit_diff -u
Loaded suite test/test_example
Started
.
Finished in 0.000926 seconds.

1 tests, 1 assertions, 0 failures, 0 errors
/opt/local/bin/ruby -I.:lib:test -rtest/unit -e "%w[test/test_example.rb].each { |f| require f }" 
| unit_diff -u
Loaded suite -e
Started
.
Finished in 0.000981 seconds.

1 tests, 1 assertions, 0 failures, 0 errors


ここで一回autotest を終了するため、ターミナルで「^C」を2回打ちます。



Column::autotest の動作

直前でファイルを修正した後に、autotest は2回のテストを実行しました。最初のテストは、変更箇所に対応するテスト、次のテストは全体のテストです。
本によると、一般にautotest の動作順序は次のようになるとのことです。(この動作は、ZenTest/lib/autotest.rb に定義されています)

  • 1. テストファイルと実装ファイルを関連づける
  • 2. 全てのテストを実行
  • 3. 失敗したテストを取り出す
  • 4. ファイルの変更を検知し、変更箇所と関連するテスト、及び、前回失敗したテストを実行
  • 5. テストが失敗したら3 へ
  • 6. テストが成功したら2 へ
  • 7. 「^C」が1回押されたらテスト情報をリセットして2 へ
  • 8. 「^C」が2回続けて押されたら終了


rails でのautotest

rails でのautotest はRAILS_ROOT でautotest と打てば、あとは規約にそってテストが実行されます。先に述べた規約ではなく、rails の規約でテストが実行されるので、特に何もせず使うことができます。

通知のカスタマイズ

最初に、autotest の魅力に「通知」があると書きました。最後に、その通知のカスタマイズをやってみます。今回は、Growl での通知、screen での通知を試してみます。

雛形ファイルをコピー

以下のコマンドを実行します。

$ cp `gem environment gemdir`/gems/ZenTest-3.10.0/example_dot_autotest.rb ~/.autotest


この雛形ファイルを編集することで、通知方法をカスタマイズすることができます。

Growl との連携

なお、ここでは「autotestのGrowl通知をカスタマイズする - ザリガニが見ていた...。」を参考にしました。

  • Growl, growlnotify のインストール

あらかじめ、Growl とgrowlnotify をインストールしておきます。インストールの方法は、以下のblog を参照。

  • 通知用画像のダウンロード

テスト成功時、失敗時の通知に使える画像があるのでDL しておきます。

$ mkdir ~/.autotest_image
$ cd ~/.autotest_image
$ curl -O http://blog.internautdesign.com/files/rails_ok.png
$ curl -O http://blog.internautdesign.com/files/rails_fail.png

  • .autotest を以下のように編集します
# -*- ruby -*-
require 'autotest/growl'

module Autotest::Growl
  IMG_OK = '~/.autotest_image/rails_ok.png'
  IMG_FAIL = '~/.autotest_image/rails_o.png'
  
  class << self
    undef growl
    
    def growl(title, msg, img = nil, pri=0, sticky="")
      msg += " at #{Time.now.strftime('%Y-%m-%d %H:%M:%S')}"
      img ||= "/Applications/Mail.app/Contents/Resources/Caution.tiff"
      system "growlnotify -n autotest #{title} -m #{msg.inspect} --image #{img} -p #{pri} #{sticky}"
    end      
  end
  
  Autotest::HOOKS[:red].clear
  Autotest::HOOKS[:green].clear
  Autotest::HOOKS[:all_good].clear

  Autotest.add_hook :ran_command do |at|
    output = at.results.join
    failed = output.scan(/^\s+\d+\) (Failure|Error):\n(.*?)\((.*?)\)/)
      
    if failed.size == 0
      growl "Tests Passed", "Tests passed", IMG_OK, -2
    else
      f,e = failed.partition { |s| s[0] =~ /Failure/ }
      growl "Tests Failed", "#{f.size} Failures  #{e.size} Errors", IMG_FAIL, 2
    end
  end
end


さらに、Growl を以下のように設定しました。

  • 表示オプション>緊急 を赤


テストの表示結果は以下のようになりました丶(´▽`)ノ



screen との連携

Leopard にはscreen がプリインストールされているので、そのまま使えて素敵です。ここでは、Autotest::Screenが便利な件について。 (SaikyoLine.jp)を参考にしました。

  • .autotest
# -*- ruby -*-

require 'autotest/screen'

Autotest::Screen.statusline = "[%m/%d %02c] %{=r dd}%-w%{=b bw}%n %t%{-}%+w %="
# Autotest::Screen.statusline = %q[%{=r dd} %-w%{=b dd}[%n] %t %{-}%+w %=]

class Autotest::Screen
  Autotest.add_hook :run_command do |at|
    message 'Testing...' if execute?
  end

  Autotest.add_hook :ran_command do |at|
    if execute? then
      output = at.results.join
      failed = output.scan(/^\s+\d+\) (Failure|Error):\n(.*?)\((.*?)\)/)

      if failed.size == 0 then
        message "All Green", :green
      else
        f,e = failed.partition { |s| s[0] =~ /Failure/ }
        message "Red F:#{f.size} E:#{e.size}", :red
      end
    end
  end
end

表示結果は以下のようになりました。




個人的には、screen との連携の方がよさそうなのでこっちを使うことにします。Growl だとたまに見逃しそうなので。