最近は Ruby のテストに興味があっていろいろ試しています。

今気になっているのは RSpec と Cucumber の2つ。今回はまず RSpec を色々触ってみたのでそのときのログをメモってみます。RSpec については RSpec + Autotest::screen = 最高の開発環境 でも書きましたが、BDD(振舞駆動開発)のフレームワークで、describe と it という2つのメソッドを利用します。describe にテストしたい振舞を書き、it にはそのときに満たすべき仕様を書くという感じです。今回は Rails で RSpec を使ったテストを書いてみましたよ。(=゚ω゚)ノ

事前準備として、rspec と rspec-rails と Zentest(テストを自動で走らせるため。この中に autotest が含まれています)をインストールします。

sudo gem install rspec rspec-rails Zentest

まず、テストに使う model はこんな感じにしてみました。blog has many tags ( tag belongs to blog ) です。あとは get_hatebu っていうはてブ数を取得してくれるメソッドだけ実装。

# models/blog.rb
class Blog < ActiveRecord::Base
  has_many :tags

  require "xmlrpc/client"
  require "uri"
  require "net/http"
  Net::HTTP.version_1_2
  HATENA_XMLRPC_URL = "http://b.hatena.ne.jp/xmlrpc"

  def self.get_hatebu(url)
    uri = URI.parse(HATENA_XMLRPC_URL)
    server = XMLRPC::Client.new(uri.host, uri.path, uri.port)
    return server.call("bookmark.getTotalCount", url)
  end
end

# models/tag.rb
class Tag < ActiveRecord::Base
  belongs_to :blog
end

次はspecファイル(RSpec のテストコードを書いたファイル)です。テスト(というか仕様)は7つ作りました。1つのitブロックが1つのテスト(仕様)に対応します。beforeには、各itブロック実行毎に走らせたい事前処理を書きます。

spec/models/blog_spec.rb
require File.expand_path(File.dirname(__FILE__) + '/../spec_helper')

describe "Blogデータを保存する場合" do
  before do
    @blog = Blog.new( :url => "http://sasata299.com" )
  end
  
  it "新たなデータが作成できること" do
    @blog.save.should be_true # 成功すればtrue、失敗すればfalse
  end
  
  it "DBに記事が1件増えること" do
    Proc.new { @blog.save }.should change(Blog, :count).by(1)
  end
end

describe "Blogデータを一覧表示する場合" do
  fixtures :blogs, :tags

  before do
    @blog = Blog.find(:all)
  end
  
  it "全部で3件のブログが登録されていること" do
    @blog.size.should == 3
  end
  
  it "sasata299's blog はタグを2つ持つこと" do
    pending("hoge")
    blogs(:my_blog).should have(2).tags
  end
  
  it "sasata299's blog は perl と mysql というタグを持つこと" do
    blogs(:my_blog).tags.should == [ tags(:tag_1), tags(:tag_4) ]
  end
end

describe "はてブ数を取得する場合" do
  it "1つでもブックマークがあれば、その数を取得できること" do
    Blog.get_hatebu("http://blog.livedoor.jp/sasata299/").should >= 290
  end
  
  it "1つもブックマークされていなければ、0が返ること" do
    Blog.get_hatebu("http://blog.livedoor.jp/sasata2999/").should == 0
  end
end

fixtureファイルはこんな感じー。

spec/fictures/blogs.yml
my_blog:
  id: 1
  name: "sasata299's blog"
  url: http://blog.livedoor.jp/sasata299/
zozom:
  id: 2
  name: zozomのページ
  url: http://www.zozom.net
sasata299:
  id: 3
  name: sasata299のページ
  url: http://sasata299.com

何度も言いますが、itブロックでは仕様を定義しています。つまり、こうなるべき(should)という期待結果があるはずです。それをコードの中でごく自然に書けるようにするため、必ずこのように should (もしくは should_not) という形で期待結果と値を比較するわけです。分かりやすいですね〜。

hoge.should マッチャー
    or
fuga.should_not マッチャー

== とか have とかがマッチャーです。マッチャーについては RSpecの標準Matcher一覧表 - Text::Easyhacking がまとまっていてオススメですよ!

んで、実行結果はこのようになります。僕の場合は Autotest::screen を使っているので、自動でテストが走り screenのステータスライン上(画面右下)に結果が表示されます。autotest の設定ファイルはここに張ってあるので良かったら参考にしてみてください。

/usr/bin/ruby -S spec/models/blog_spec.rb -O spec/spec.opts

はてブ数を取得する場合
- 1つもブックマークされていなければ、0が返ること
- 1つでもブックマークがあれば、その数を取得できること

Blogデータを一覧表示する場合
- sasata299's blog は perl と mysql というタグを持つこと
- sasata299's blog はタグを2つ持つこと (PENDING: hoge)
- 全部で3件のブログが登録されていること

Blogデータを保存する場合
- DBに記事が1件増えること
- 新たなデータが作成できること

Pending:
Blogデータを一覧表示する場合 sasata299's blog はタグを2つ持つこと (hoge)
  Called from spec/models/blog_spec.rb:30

Finished in 1.113638 seconds

7 examples, 0 failures, 1 pending
==============================================================================

Rails を使っている場合 spec ディレクトリの中に spec.opts というファイルがあります。これが RSpec の設定ファイルです。このファイルを↓のようにしておくことで、結果が↑のようになって見やすいです。

--colour
--format specdoc # ココを変更しました
--loadby mtime
--reverse

あと、Zentest の中の autotest を使っていて、ファイル(xxx.rb)を変更すると対応するspecファイル(xxx_spec.rb)のテストを実行してくれるのはとても便利なんですが、controller のテストとかはあんまり要らないかなぁと思ったので、このようにいじって model しか自動でテストを走らせないようにしています。

ただ、元ファイルを弄るっていうのが何か凄くアレです。。。「こうすれば出来るよ」など知っている方いましたら是非教えてください _ノ乙(、ン、)_

autotest.rb
def find_files
  result = {}
  targets = self.find_directories + self.extra_files
  self.find_order.clear

  targets.each do |target|
    order = []
    Find.find(target) do |f|
      Find.prune if f =~ self.exceptions

      next if test ?d, f
      next if f =~ /(swp|~|rej|orig)$/
      next if f =~ /\/\.?#/
      next if f !~ /model/ # この行を追加!
                           
      filename = f.sub(/^\.\//, '')

      result[filename] = File.stat(filename).mtime rescue next
      order << filename
    end
    self.find_order.push(*order.sort)
  end

  return result
end

最後に、今回色々と触ってみて「なるほど〜」と思ったことをつらづらとまとめておきます。参考に。

# こんな風に指定すると、fixturesを利用できる
# テスト時にfixturesのデータをテーブルにinsertし、終わったら消してくれる
fixtures :blogs, :tags

# fixturesのデータにアクセスするにはこのような指定ができる
blogs(:my_blog)
tags(:tag_4)

# テストをpending(保留)したいときにはpendingメソッドを指定する
pending(msg)

# changeマッチャが便利!!
# Procオブジェクト(xxx)を評価したら、yyyの値がzzzだけ変化する
# byの他にも、fromとかtoもある
Proc.new { xxx }.should change(Obj, :yyy).by(zzz)

やっぱり RSpec でテストを書くと分かりやすいし、見た目もすっきりします。autotest を使って自動でテストを走らせるのもとてもいいです。この2つを同時に使うと素晴らしいですね〜。

さぁ、次は Cucumber について書きますよ!!
このエントリーをはてなブックマークに追加