RSpec on Railsを使ってみた

WEB+DB PRESS Vol.45のBDD特集を教科書に、rspec-railsを扱ってみた。教科書があることが前提なので、これを買って読んでいない人には意味不明な記事になると思う。

ただし、完全に本と同じやり方をしてはいない。最新バージョンを使うからインストール方法などいろいろと違うし、Seleniumも使わない。また、本では触れていない事柄についても少し調べた。そんなわけで、本の通りにやらなかったところをメモ書きしておきたい。

環境は次の通り。

アプリケーション作成

いつも通り。本ではPostgreSQLを使っていたが、華麗に無視してSQLite3を使う。簡単。

$ rails bbs
$ cd bbs

rspec-railsインストール

せっかくなんで、流行のGitを使った。たぶんRubyforgeに行ってrspec-rails-1.1.4.tgzを落としてきて使っても問題ない。

最新の安定版1.1.4を使いたいのだが、GitHubを見に行ったところ、どうやらバージョンを指定してインストールするためには、たくさんコマンドを叩く必要があるらしい。

$ cd vendor/plugins
$ git clone git://github.com/dchelimsky/rspec.git
$ git clone git://github.com/dchelimsky/rspec-rails.git
$ cd rspec
$ git checkout 1.1.4
$ cd ../rspec-rails
$ git checkout 1.1.4
$ cd ..
$ rm -rf rspec/.git
$ rm -rf rspec-rails/.git
$ cd ../../

gitでリポジトリのクローンを取得、バージョン(というかタグ)指定して1.1.4のコードを取得、そこまでできたらリポジトリは不要なので削除、という流れ。

次のコマンドを実行して、基本的なファイルの生成を行う。

$ script/generate rspec

ちなみにバージョン指定せずEdgeをインストールするのであれば実行するコマンドの数が減るのだが、試してみたところファイル生成時にdefault_valueメソッドが無いとのエラーが出てしまう。

Model作成

$ script/generate rspec_model Post content:text name:string

ここらでDBを作っておく。SQLite3だとrake db:createがいらない。

$ rake db:migrate

あとは本にあるとおりにコードを書いていけば、ちゃんとテストできる。fixtureとかは好き勝手につくる。もっとも、作らなくてもとりあえずテストはグリーンになるが。

テスト実行時にDEPRECATION WARNINGが出てしまうのだが、とりあえず不都合は無いので放置。rspec-rails内のコードがdeprecatedなメソッドを呼び出しているらしいが、まぁそのうち対応されるだろう。

Controller作成

これも本の通りでOK。

View作成

本にあるspecコードなんだが。

it "は、投稿の表示(div.post)を含むこと" do
  response.should have_tag('div') do |div|

これ、定義文(div.post)とコード(div)が一致していないのでは?たぶんこう書くべきだと思うのだが。

it "は、投稿の表示(div.post)を含むこと" do
  # response.should have_tag('div') do |div|
  response.should have_tag('div.post') do |div| # こっちか?

さてテストしてみようかな、と思うわけだが、本にはViewのspecは書いてあるもののテスト対象のhtml.erbファイルについては何も書かれていない。そんなわけで、app/views/posts/list.html.erbを適当に作る。こんな感じでいいや。

<h1>BBS for RSpec trial</h1>

<% @posts.each do |post| %>
  <div class="post">
    <div class="name"><%=h post.name %></div>
    <div class="content"><%=h post.content%></div>
  </div>
  <hr/>
<% end %>

テストしてみると、見事にエラー。

1)
ActionView::TemplateError in 'posts/listテンプレートがpostのリストを渡されたとき は、投稿の表示(div.post)を含むこと'
Mock 'post 1' received unexpected message :name with (no args)

本にある通りのspecだと、mockにname, contentメソッド呼び出しがあることを教えていないからだと思われる。ちゃんと教えてやるか。

require File.dirname(__FILE__) + '/../../spec_helper'

describe "posts/listテンプレートがpostのリストを渡されたとき" do
  before { assigns[:posts] = @posts = (1..5).map { |i| mock("post #{i}") } }

  # このbeforeを足した。
  before {
    @posts.each_with_index do |p,i|
      p.should_receive(:name).and_return("name of #{i}")
      p.should_receive(:content).and_return("content of #{i}")
    end   
  }

  before { render 'posts/list' }

...

なんかとてつもなくいい加減な値を返しているので、ほんとにこんなんでいいのか?という疑問が湧いてしまう…。

気を取り直してテストしてみると、今度は無事グリーンに。

しかし、ここでちょいとrspec-railsのrdocを眺めてみると、with_tag / without_tagというメソッド発見。

もしかして…

  it "は、投稿の表示(div.post)を含むこと" do
    response.should have_tag('div.post') do |div|
      div.should have_tag('div.name')
    end
  end

ここは書き換えられるのか。

  it "は、投稿の表示(div.post)を含むこと" do
    response.should have_tag('div.post') do
      with_tag('div.name')
    end
  end

試した感じ、どちらも同じ動作をしていそう。しかし、with_tagの方がわかりやすい。

カバレッジ

ところで、どうせテストするならやっぱりカバレッジを知りたいところ。ヘルプを見た感じ、Rcovを簡単に使えそうだ。

まずはRcovをインストール。

$ sudo gem install rcov

続けて、カバレッジ取得。

$ rake spec:rcov

これでcoverage/にレポートがHTML形式で生成されるので、これを適当なブラウザで眺めるとよし。

RSpecのHTMLレポート

RSpecはテスト結果をHTMLフォーマットで出力する機能をデフォルトで持っている。しかし、rspec-railsではプレーンテキストベースのprogressフォーマットとなっている。これ、切り替えできないものだろうか。

単にHTMLフォーマットに変更してしまうのであれば、spec/spec.optsの中身を書き換えれば済むことだ。しかし、HTMLフォーマットのレポートは基本的に継続的インテグレーションしている場合のビルドレポートの一部として見たいだけで、通常開発時は元の通りprogressフォーマットでレポートを標準出力に出してほしい。つまり、両方使いたい。

これは簡単な対処法は無いみたいで、どうも自分で独自にタスクを定義するしかなさそう。そんなわけで、lib/tasks/rspec_ext.rakeを作成。中身はこんな感じ。

rspec_base = File.expand_path(File.dirname(__FILE__) + '/../../vendor/plugins/rspec/lib/')
$LOAD_PATH.unshift(rspec_base) if File.exist?(rspec_base)
require 'spec/rake/spectask'

spec_prereq = File.exist?(File.join(RAILS_ROOT, 'config', 'database.yml')) ? "db:test:prepare" : :noop
task :noop do
end

namespace :spec do
  desc "Generate HTML report for Rspec."
  Spec::Rake::SpecTask.new(:html_report => spec_prereq) do |t|
    t.fail_on_error = false
    t.spec_opts = ["--format", "h:spec/report.html"]
    t.spec_files = FileList['spec/**/*_spec.rb']
  end
end

noopタスクの定義部分までは、vendor/plugins/rspec-rails/tasks/rspec.rakeの中身をコピーした。これで次のようにコマンドを叩けば…。

$ rake spec:html_report

spec/report.htmlが生成されるわけである。lib/tasks/rspec_ext.rakeの中身をそのままrspec.rakeの中に追記してもいいような気もするが、どうもプラグイン本体に手を入れるのには抵抗がある。

まとめ

とりあえず、大ざっぱに把握。TDDには無い、BDDの気持ちよさが感じられた。また、カバレッジ測定にHTMLレポート作成と、やりたいことは一通りできる。

仕様を記述するという、より人間サイドに近いスタンスを取っているだけあって、なんだか普通のテストコードよりも自然に書ける気がする。そして、記述様式の変更に止まらない、テストを強力にサポートする数多くの機能も実に頼もしい。

しかし、RSpecのドキュメントを眺めていると、本当に多彩な機能が組み込まれているようで、とても一朝一夕でマスターできるような代物じゃなさそうだ。ちゃんとドキュメント読み込んで、バリバリ使っていかないとな。