RSpec 1.3でカスタムマッチャ

ある処理で行われるファイルの移動が適切かどうかテストしたかったので、カスタムマッチャを作ってみた。使用したのはRSpec 1.3.1。

目標のspec

lambda { foo }.should move_file(src, dest)

チェックする項目

  1. fooメソッドを実行する前にsrcにファイルが存在することをチェック(事前チェック)
  2. Foo#barを実行した後にsrcにファイルが存在しないことをチェック(事後チェック)
  3. Foo#barを実行した後にdestにファイルが存在することをチェック(事後チェック)
  4. srcとdestのMD5値が一致することをチェック(事後チェック)

(1は要らないかも?)

実際のコード

module CustomFileMatchers
#
# 1つのマッチャを処理するためのクラス
#
class MoveFile
def initialize(src, dest)
@src = src
@dest = dest
end
#
# xxx.should ...としたときのxxxがtrialとして与えられるので、
# これがテストに通るか否かをbooleanで返す
#
def matches?(trial)
src_digest = digest_of(@src)
unless FileTest.exist?(@src)
@error = "source file didn't exist"
return false
end
trial.call
unless !FileTest.exist?(@src)
@error = "source file still exists"
return false
end
unless FileTest.exist?(@dest)
@error = "destination file doesn't exist"
return false
end
unless src_digest == digest_of(@dest)
@error = "destination file's MD5 checksum doesn't match with source's one"
return false
end
true
end
#
# テストに通らなかったときにエラーメッセージを
# 取得するために呼ばれる
#
def failure_message_for_should
"expected #{@src} to move to #{@dest} (#{@error})"
end
private
def digest_of(path)
str = ""
File.open(path, "rb") do |f|
buf = ""
while f.read(256, buf)
str << buf
end
end
Digest::MD5.hexdigest(str)
end
end
#
# xxx.should ...の...の部分で、
# マッチャを処理するクラスをインスタンス化して返す
#
def move_file(src, dest)
MoveFile.new(src, dest)
end
end

CustomFileMatchers::MoveFileのmatches?で具体的なテストを実行する。

使い方

describe文のなかでincludeして使う。 ためしにちゃんと判定できているかもspecで書いてみた。 (it文の内容が微妙かもしれない)

describe CustomFileMatchers do
include CustomFileMatchers
describe "::MoveFile" do
it "should pass when a file moved" do
src = "#{RAILS_ROOT}/tmp/a.txt"
dest = "#{RAILS_ROOT}/tmp/b.txt"
FileUtils.touch src
lambda { FileUtils.mv src, dest }.should move_file(src, dest)
end
it "should NOT pass when a file didn't move" do
src = "#{RAILS_ROOT}/tmp/a.txt"
dest = "#{RAILS_ROOT}/tmp/b.txt"
FileUtils.touch src
lambda {}.should_not move_file(src, dest)
end
end
end

まとめ

意外と簡単にできたし、specのコードがコンパクトかつそれだけで意味が通じるようになるのでカスタムマッチャはとても便利。

複数のチェック項目が通って一つの意味を成す場合にとても強力。

change.from.toのように条件の追加をメソッドチェインしたければCustomFileMatchers::MoveFileに引数をインスタンス変数に保存するようなメソッド(changeでいうところのfromやto)を追加して、matches?でそれらをチェックすればいいのだと思われる。

参考資料