RSpec 1.3でカスタムマッチャ
ある処理で行われるファイルの移動が適切かどうかテストしたかったので、カスタムマッチャを作ってみた。使用したのはRSpec 1.3.1。
目標のspec
lambda { foo }.should move_file(src, dest)
チェックする項目
- fooメソッドを実行する前にsrcにファイルが存在することをチェック(事前チェック)
- Foo#barを実行した後にsrcにファイルが存在しないことをチェック(事後チェック)
- Foo#barを実行した後にdestにファイルが存在することをチェック(事後チェック)
- srcとdestのMD5値が一致することをチェック(事後チェック)
(1は要らないかも?)
実際のコード
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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文の内容が微妙かもしれない)
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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?でそれらをチェックすればいいのだと思われる。