読者です 読者をやめる 読者になる 読者になる

最近やってるRailsプロジェクトのテスト方法

Railsエンジニアになってから1年半くらいが経ち、社内のRailsのプロジェクトを全部で5つくらい触って、今やってるAbilie*1でようやく人並みにテストを書いてる気がしてきたので、現時点でやってるテストの方法をまとめておく。

テストのルール的なの

rspecでは必ずモデルのテストは書くようにしてる。ヘルパーも大体書いてるけど、コントローラやルーティングのテストはあまり書いてない。
というのも、コントローラーのコードを極力短くしてモデルを太らせているのでコントローラのテストはあんまり意味が無い気がしていて、その代わりにCapybaraでテストを書いておけば十分なんじゃないかなと思ってきたから。Capybaraは書いてるので、そういう意味では書いてるとも言える。
社内の管理者だけが使える管理画面も作ってるけど、そっちはテストあんまり書いてない。ここは動かなくなっても一般ユーザーには影響が無いので、動かなくなったのを気づいてから直せばいいかなーという感じ。

テストの実行

GuardとSporkを組み合わせて使い、結果をGrowlで出力してる
guard/guard · GitHub
guard/guard-spork · GitHub

Guardはファイルに変更があったらなにかしてくれるもの。
Sporkは事前にRailsのコードを事前に読み込んでおいてrspecの起動時間を短縮してくれる。
GrowlはMacだったらみんな使ってるアレですね。最近有料になりましたが、会社に #金くれ って言ったら買ってくれました。
Guardに関して紹介してるブログとか読むと、guardを起動時に全体のテストを流し、モデルなどのテストを通ったらまた全体のテストを流すように設定してあるけど、全体テストが流れると時間がかかるので設定を無効化してる
Guardfileを以下のように書けばいい。

guard 'rspec', :version => 2, :cli => "--drb", :all_after_pass => false, :all_on_start => false do

全体テストが実行したくなったら、guardが動いてる所でEnterを押せば動く。

fixture_replacement

factory_girlで特に困ってもないので相変わらずfactory_girlを使ってる。
thoughtbot/factory_girl · GitHub

modelを作成したときにfactory_girlのファイルも生成するようにconfig/application.rbに以下のように書いてる

config.generators do |g|
  g.test_framework :rspec, :fixture => true, :fixture_replacement => :factory_girl
  g.fixture_replacement :factory_girl, :dir => 'spec/factories'
end 

あとSporkを実行してると、factory_girlのファイルに変更があっても更新してくれないので、spec_helper.rbに以下のように書いてる。

Spork.each_run do
  FactoryGirl.reload
  Deadend::Application.reload_routes!
end

カバレッジ

半年くらい前はcover_meを使ってたけど、最近はsimple-covを使ってる。
colszowka/simplecov · GitHub

出力されたカバレッジ結果のHTMLが綺麗なのと、rcov形式で出力できてJenkinsでも使えるのが良い。
Sporkで実行される場合は何か変になるので、Sporkを使ってる時は実行しないようにしてる。
あとローカルでrake specしたときはデフォルトの出力を、CIで実行した場合にはrcov形式で出力できるようにspec_helper.rbに以下のように書いてる。

unless Spork.using_spork?
  require 'simplecov'
  if ENV["JENKINS"] == 'on'
    require 'simplecov-rcov'
    SimpleCov.formatter = SimpleCov::Formatter::RcovFormatter
  end

  SimpleCov.start 'rails' do
    add_filter "/spec/"
  end
end

Redisのテスト

resqueを使ってるというのもあり、プロジェクトでRedisを使っているのでこのテストも書きたい。
RSpec and Resque · defunkt/resque Wiki · GitHub
resqueのWikiを参考に、テスト実行時にredisのconfファイル作ってredisを起動し、テストが完了後にプロセスを落としてダンプファイルも消してる。
Jenkinsで動かす場合にうまく動かなかったのでその部分だけTempFileを使うようにしてる。

REDIS_PID = "#{Rails.root}/tmp/pids/redis-test.pid"
REDIS_CACHE_PATH = "#{Rails.root}/tmp/cache/"

config.before(:suite) do
  redis_options = {
    "daemonize"     => 'yes',
    "pidfile"       => REDIS_PID,
    "port"          => 9736,
    "timeout"       => 300,
    "save 900"      => 1,
    "save 300"      => 1,
    "save 60"       => 10000,
    "dbfilename"    => "dump.rdb",
    "dir"           => REDIS_CACHE_PATH,
    "loglevel"      => "debug",
    "logfile"       => "stdout",
    "databases"     => 16
  }.map { |k, v| "#{k} #{v}" }

  if ENV["JENKINS"] == 'on'
    require 'tempfile'
    temp = Tempfile::new("redis_temp.conf", REDIS_CACHE_PATH)
    redis_options.each do |value|
      temp.puts(value)
    end

    temp.close

    Subexec.run("redis-server #{temp.path}")
  else
    redis_options = redis_options.join("\n")
    `echo '#{redis_options}' | redis-server -`
  end
end


config.after(:suite) do
   %x{
     cat #{REDIS_PID} | xargs kill -QUIT
     rm -f #{REDIS_CACHE_PATH}dump.rdb
   }
end

WebAPIのテスト

ここで言うWebAPIのテストとは、外部のWebAPI(twitterとか)を使う場合のテストのこと。
これはwebmockというgemを使えば良い。
bblimke/webmock · GitHub

ただ、webmockを使ってしまうとCapybaraがlocalhostにアクセスしたときにもすべてmockが返ってきてしまうので、localhostを除外するようにする
spec_helper.rbに以下のように書く

WebMock.disable_net_connect!(:allow_localhost => true)

Javascriptのテスト

Capybara-Webkitを使ってる。
thoughtbot/capybara-webkit · GitHub
今のところそんな特殊なJavascriptを書いてるわけではなく、ほとんどjQueryに頼ったものなのでこれで十分間に合ってる。
ただ、隣の人からJSのカバレッジも見たいという要望も出てるので、poltergeistを使ったほうが良いのかもしれない。
jonleighton/poltergeist · GitHub

あとCapybara-Webkitrspecとは別のスレッドで動くらしく、factory_girlで作ったデータをCapybara側で読めない。
CapybaraのgithubでActiveRecordのshared_connectionを変える方法が紹介されてるけど、なんかうまく行かなかったのでDatabaseCleanerを使う方法をとってる。
JSのテスト以外は従来のtransactionを使ってデータを消し、JSの時はtruncateを全テーブルにする方法にしてる。

Capybara.javascript_driver = :webkit

config.before :each do
  if Capybara.current_driver == :rack_test
    DatabaseCleaner.strategy = :transaction
    DatabaseCleaner.start
  else
    DatabaseCleaner.strategy = :truncation
  end
end

config.after :each do
  DatabaseCleaner.clean
end

画像のテスト

画像関連はCarrierWaveを使ってるんだけど、factory_girlで作られてしまうのでテストが終わった後に消してる。あとテストの場合は通常とは違うディレクトリに書き出してる。

継続的インテグレーション

Jenkinsを使ってgitにpushされたらテストを実行してる。それとは別に朝8時ごろにも流してる。
simplecov-rcovとci_reporterを使って、カバレッジとテスト数の推移をグラフで出してる。
またテストに通ったら、自動的に社内の検証環境にデプロイされるように、ビルドパイプラインの設定をしてる。
Abilieに関してはまだリリースされてないので、Jenkinsも試験的に社内のサーバに入れてるだけで、本番へのデプロイとかはやるかどうかわからない。
まだJenkinsは使い始めたばかりであまりいじってないけど、rails_best_practiceとかも組み込めるといいかなと思ったりしてる。

おわりに

入社した直後に引き継いだプロジェクトはテストも無く、チームにテストを書く文化もなく、業務でちゃんとテストを書いたことがなかったぼくですが、入社半年後くらいからテストを書き始め、少しずつテストを書く文化ができてきてよかったかなと。
とは言っても僕の担当してるプロジェクト以外は相変わらずテストが書かれてないものが多いので、そのあたりが今年の課題な気はしてる。

*1:詳しくはhttp://d.hatena.ne.jp/tohae/20111222/1324533072