はじめに
RSpec 3が正式リリースされて2ヶ月ほど経過しました。(正式リリースは2014年6月)
ネットの情報を見ていると、これまでは「既存のテストケースをRSpec 3にアップグレードさせる方法」や「RSpec 3で削除されたり、記法が変わったりした点」など、「守りの姿勢」に入った情報が多かったように思います。(僕自身もそういう情報をたくさんアップしていました)
しかし、RSpec 3では以前のバージョンでは使えなかった新しい機能も数多く導入されています。
そこで本記事では「攻めの姿勢」で「RSpec 3から導入された新機能」をまとめてみました。
なお、ここでフォーカスするのはテストコードの書き方にダイレクトに関わってくるマッチャの新機能です。
2015.01.12:RSpec 3.1に関する情報を追記しました
RSpec 3.1に関する情報も追記しました。
もともと紹介していた新機能は8つでしたが、現在は1つ増えて 9つ になっています。
2015.06.22:RSpec 3.3の新機能は別記事で紹介しています
RSpec 3.3の新機能は見所が多かったので別記事としてまとめてあります。
こちらもあわせてどうぞ。
実用的な新機能が盛りだくさん!RSpec 3.3 完全ガイド - Qiita
本記事で紹介する新機能の一覧
本記事では以下のような新機能を紹介します。
- マッチャ同士を
and
やor
で連結できる 「マッチャ合成式」 - マッチャの引数に別のマッチャが渡せる 「コンポーザブルマッチャ」
- テストコードや出力結果の可読性を向上させる 「マッチャエイリアス」
- ハッシュや配列にも使えるようになった 「
match
マッチャ」 - (順番は無視して)配列の中身を検証する 「
contain_exactly
マッチャ」 - コレクションの全要素がtrueになることを検証する 「
all
マッチャ」 - exclusiveモードが追加された 「
be_between
マッチャ」 - 標準出力や標準エラー出力の出力結果を検証する 「
output
マッチャ」 - オブジェクトの属性(プロパティ)を検証する 「
have_attributes
マッチャ」(RSpec 3.1)
参考文献
本記事は以下の参考文献を僕なりにまとめたものです。
サンプルコード等はこちらから拝借しています。
- New in RSpec 3: Composable Matchers (日本語訳)
- Notable Changes in RSpec 3 (日本語訳)
- RSpec 3.1 has been released!
1.マッチャ同士を and や or で連結できる「マッチャ合成式」 (Compound Matcher Expressions)
RSpec 3ではand
またはor
を使って、マッチャを連結することができます。
これにより、「XがAかつBかつCであること」や「AまたはBまたはCであること」を一度に検証できるようになります。
使い方は以下のサンプルコードを見てください。
it 'アルファベットはaで始まり、かつzで終わること' do
# RSpec 2
expect(alphabet).to start_with("a")
expect(alphabet).to end_with("z")
# RSpec 3
expect(alphabet).to start_with("a").and end_with("z")
# &を使うことも可能
expect(alphabet).to start_with("a") & end_with("z")
end
it '信号の色は赤、または緑、または黄色であること' do
# RSpec 3
expect(stoplight.color).to eq("red").or eq("green").or eq("yellow")
# |を使うことも可能
expect(stoplight.color).to eq("red") | eq("green") | eq("yellow")
end
ブロックを受け取るマッチャでも and や or が使える(RSpec 3.1)
RSpec 3.1からは change
のように、ブロックを受け取るマッチャでも and や or が使えるようになりました。
この機能を使えば「A という操作をすると、X が X' に、Y が Y' にそれぞれ変更される」というようなケースでテストが書きやすくなります。
it 'xとyの値が同時に変更されること' do
x = y = 0
expect {
x += 1
y += 2
}.to change { x }.to(1).and change { y }.to(2)
end
2.マッチャの引数に別のマッチャが渡せる「コンポーザブルマッチャ」 (Composable Matchers)
RSpec 3ではマッチャの引数にマッチャを渡せます・・・と言われてもピンとこないと思うのでサンプルコードを見てください。
class BackgroundWorker
attr_reader :queue
def initialize
@queue = []
end
def enqueue(job_data)
queue << job_data.merge(:enqueued_at => Time.now)
end
end
describe BackgroundWorker do
it 'キューにジョブが順番通りに追加されること' do
worker = BackgroundWorker.new
worker.enqueue(:klass => "Class1", :id => 37)
worker.enqueue(:klass => "Class2", :id => 42)
# RSpec 2
expect(worker.queue.size).to eq(2)
expect(worker.queue[0]).to include(:klass => "Class1", :id => 37)
expect(worker.queue[1]).to include(:klass => "Class2", :id => 42)
# RSpec 3
expect(worker.queue).to match [
include(:klass => "Class1", :id => 37),
include(:klass => "Class2", :id => 42)
]
end
end
例えば、上の例ではmatch
マッチャに2つのinclude
マッチャを配列として渡しています。
# matchマッチャに2つのincludeマッチャを配列として渡す
expect(worker.queue).to match [
include(:klass => "Class1", :id => 37),
include(:klass => "Class2", :id => 42)
]
これにより、worker.queue
が2要素の配列であることと、その配列の中身が期待通りのハッシュであることを同時に検証できます。
ちなみに、このサンプルコードでは match
マッチャで配列の中身を検証していますが、これもRSpec 3から使えるようになった新機能です。詳しくは後述します。
注意: match[...]
ではなくmatch [...]
またはmatch([...])
と書いてください
match
と[
の間にスペースが入っていることに注目してください。
これをmatch[
と続けて書いてしまうとRubyの文法上、match
の戻り値に対して[]
メソッドを呼ぶことになってしまいます。
そうではなく、ここはmatch
の引数として配列を渡す必要があるので、match
と[]
の間にスペースを入れる必要があります。
スペースのあり・なしで挙動が変わるのが気持ち悪い人は、match([...])
のように丸括弧で[]
を囲むようにすると確実です。
# エラー
# expect(worker.queue).to match[
# include(:klass => "Class1", :id => 37),
# include(:klass => "Class2", :id => 42)
# ]
# match と [ の間にスペースを入れる
expect(worker.queue).to match [
include(:klass => "Class1", :id => 37),
include(:klass => "Class2", :id => 42)
]
# もしくは丸括弧で囲む
expect(worker.queue).to match([
include(:klass => "Class1", :id => 37),
include(:klass => "Class2", :id => 42)
])
コンポーザブルマッチャが使えるマッチャの一覧
コンポーザブルマッチャが使えるマッチャ、すなわちマッチャを引数として受け取れるマッチャはmatch
だけに限りません。
コンポーザブルマッチャが使えるマッチャは以下の通りです。
- change { }.by(matcher)
- change { }.from(matcher).to(matcher)
- contain_exactly(matcher, matcher, matcher)
- end_with(matcher, matcher)
- include(matcher, matcher)
- include(:key => matcher, :other => matcher)
- match(arbitrary_nested_structure_with_matchers)
- output(matcher).to_stdout
- output(matcher).to_stderr
- raise_error(ErrorClass, matcher)
- start_with(matcher, matcher)
- throw_symbol(:sym, matcher)
- yield_with_args(matcher, matcher)
- yield_successive_args(matcher, matcher)
ただ、このリストだけ見ても活用の仕方がイマイチわからないと思うので、気になる方は下記ページに載っているサンプルコードを読むと良いと思います。
New in RSpec 3: Composable Matchers (日本語訳)
なお、contain_exactly
や output
はRSpec 3から登場した新しいマッチャです。詳しくは後述します。
3.テストコードや出力結果の可読性を向上させる「マッチャエイリアス」 (Matcher aliases)
RSpec 3では a_[type of object]_[verb]ing
という形式のマッチャエイリアス(マッチャの別名)が数多く用意されています。
it "aで始まる文字列が別の何かに変わること" do
x = "a"
# RSpec 2
expect { }.to change { x }.from start_with("a")
# エラーメッセージ
# => expected result to have changed from start with "a", but did not change
# RSpec 3
expect { }.to change { x }.from a_string_starting_with("a")
# エラーメッセージ
# => expected result to have changed from a string starting with "a", but did not change
end
上の例ではstart_with
のエイリアスであるa_string_starting_with
を使っています。
他にも、コンポーザブルマッチャで使ったサンプルコードも次のように書き換えることができます。
describe BackgroundWorker do
it 'キューにジョブが順番通りに追加されること' do
worker = BackgroundWorker.new
worker.enqueue(:klass => "Class1", :id => 37)
worker.enqueue(:klass => "Class2", :id => 42)
expect(worker.queue).to match [
include(:klass => "Class1", :id => 37),
include(:klass => "Class2", :id => 42)
]
# include のかわりに a_hash_including を使っても良い
expect(worker.queue).to match [
a_hash_including(:klass => "Class1", :id => 37),
a_hash_including(:klass => "Class2", :id => 42)
]
end
end
a_[type of object]_[verb]ing マッチャの利点
a_[type of object]_[verb]ing
形式のマッチャを使うと、テストコード自体はやや冗長になりますが、以下のような利点があります。
- テストコードが英文法的に正しく読める
-
change { x }.from start_with("a")
よりも、
change { x }.from a_string_starting_with("a")
の方が英文法的には自然
-
- テスト失敗時のメッセージも英文法的に正しくなる
-
start_with
の場合
=>expected result to have changed from start with "a", but did not change
-
a_string_starting_with
の場合
=>expected result to have changed from a string starting with "a", but did not change
-
マッチャエイリアスは [verb]ing だけにすることも可能
マッチャエイリアスは [verb]ing
だけにすることもできます。
it "aで始まる文字列が別の何かに変わること" do
x = "a"
expect { }.to change { x }.from a_string_starting_with("a")
# starting_withを使っても同じ
expect { }.to change { x }.from starting_with("a")
end
describe BackgroundWorker do
it 'キューにジョブが順番通りに追加されること' do
expect(worker.queue).to match [
a_hash_including(:klass => "Class1", :id => 37),
a_hash_including(:klass => "Class2", :id => 42)
]
# includingを使っても同じ
expect(worker.queue).to match [
including(:klass => "Class1", :id => 37),
including(:klass => "Class2", :id => 42)
]
end
end
よく使いそうなマッチャエイリアス
独断でよく使いそうな a_[type of object]_[verb]ing
形式のマッチャエイリアスを以下にまとめました。
前述のように [verb]ing
だけのエイリアスを使うことも可能です。
マッチャエイリアス | オリジナルのマッチャ | 失敗時のメッセージ |
---|---|---|
a_string_matching(/foo/) | match(/foo/) | a string matching /foo/ |
a_string_including("a") | include("a") | a string including 'a' |
a_string_starting_with("z") | start_with("z") | a string starting with 'z' |
a_string_ending_with("z") | end_with("z") | a string ending with 'z' |
a_collection_containing_exactly(1, 2) | contain_exactly(1, 2) | a collection containing exactly 1 and 2 |
a_collection_including("a") | include("a") | a collection including 'a' |
a_collection_starting_with(23) | start_with(23) | a collection starting with 23 |
a_collection_ending_with(23) | end_with(23) | a collection ending with 23 |
a_hash_including(:a => 5) | include(:a => 5) | a hash including {:a => 5} |
なお、contain_exactly
はRSpec 3から登場した新しいマッチャです。詳しくは後述します。
まだまだたくさんあるマッチャエイリアス
Module: RSpec::Matchers APIを見ると、"Also known as:"というような形でマッチャエイリアスが記載されています。
もしくは僕が個人的に作成したこちらのリストを参照してください。
4.ハッシュや配列にも使えるようになった「matchマッチャ」
RSpec 2ではmatch
マッチャは文字列にしか使えませんでしたが、RSpec 3からはハッシュや配列の中身を検証する場合にも使えるようになりました。
it 'matchマッチャを使って文字列を検証する' do
# RSpec 2, RSpec 3
expect("food").to match("foo")
expect("food").to match(/foo/)
end
it 'matchマッチャを使ってハッシュや配列がネストしたハッシュの中身を検証する' do
hash = {
:a => {
:b => ["foo", 5],
:c => { :d => 2.05 }
}
}
# RSpec 3
expect(hash).to match(
:a => {
:b => a_collection_containing_exactly(
an_instance_of(Fixnum),
a_string_starting_with("f")
),
:c => { :d => (a_value < 3) }
}
)
end
match + 配列
と match_array
の違い
match
マッチャで配列を検証すると、配列の順番も期待値と一致している必要があります。
一方、match_array
マッチャで検証した場合は、要素の個数と各要素の同値性だけを検証し、順番は検証しません。
5.(順番は無視して)配列の中身を検証する「contain_exactlyマッチャ」
RSpec 3ではmatch_array
マッチャとほぼ同じ機能を提供するcontain_exactly
マッチャが追加されました。
下の例で示すように、配列の個数と各要素の同値性を検証したいときに使えます。(ただし順番は無視します)
it '配列の個数と内容が正しいこと(順番は無視)' do
x = [2, 1, 3]
# RSpec 2, RSpec 3
expect(x).to match_array([1, 2, 3])
# RSpec 3
expect(x).to contain_exactly(1, 2, 3)
end
match_array
マッチャは非推奨(deprecated)ではないので、RSpec 3でも引き続き使えます。
ちなみに、contain_exactly
が登場したのは、match_array
という名前が意味的に不明確なメソッド名だったからだそうです。
match_array
と contain_exactly
の違い
上の例を見るとわかるように、match_array
には1個の配列を渡しますが、contain_exactly
は可変長引数として任意の個数の引数を渡すことができます。
match + 配列
と match_array
/contain_exactly
の違い
match
マッチャで配列を検証すると、配列の順番も期待値と一致している必要があります。
一方、match_array
マッチャやcontain_exactly
マッチャで検証した場合は、要素の個数と各要素の同値性だけを検証し、順番は検証しません。
6.コレクションの全要素がtrueになることを検証する「allマッチャ」
RSpec 3では新しくall
マッチャが登場しました。
このマッチャはコレクションの全要素がtrueになることを検証します。
all
マッチャの引数には別のマッチャを渡してください。
たとえば以下のような感じです。
it 'すべての数値が奇数であること' do
# RSpec 3
expect([1, 3, 5]).to all( be_odd )
end
7.exclusiveモードが追加された「be_betweenマッチャ」
RSpec 2のbe_between
は単なる動的なpredicateマッチャ(対象オブジェクトのbetween?
メソッドを呼び出すだけのマッチャ)でしたが、RSpec 3では明示的にbe_between
マッチャとして実装されています。
これにより、次のような利点が得られます。
-
exclusive
モード(両端の値を含まないモード)が使えるようになります。- デフォルトは
inclusive
モード(両端の値を含むモード)です。
- デフォルトは
- 失敗時のメッセージが以下のように明示的になります。
- RSpec 2: "between?(1, 10) returned false"
- RSpec 3: "expected 11 to be between 1 and 10"
-
between?
メソッドを持っていなくても比較演算子(<, <=, >, >=)が実装されていればbe_between
マッチャを使えるようになります。
be_between
マッチャを使ったサンプルコードを以下に示します。
it '10は1以上10以下であること' do
# RSpec 2, RSpec 3
expect(10).to be_between(1, 10)
# RSpec 3
expect(10).to be_between(1, 10).inclusive
end
it '10は1より大きく10より小さい数値ではないこと' do
# RSpec 3
expect(10).not_to be_between(1, 10).exclusive
end
8.標準出力や標準エラー出力の出力結果を検証する「outputマッチャ」
RSpec 3では新しくoutput
マッチャが登場しました。
このマッチャを使うと、以下のように標準出力(stdout)や標準エラー出力(stderr)への出力結果を検証することができます。
it '標準出力、または標準エラー出力に"foo"や"bar"を出力すること' do
# RSpec 3
expect { print "foo" }.to output("foo").to_stdout
expect { print "foo" }.to output(/fo/).to_stdout
expect { warn "bar" }.to output(/bar/).to_stderr
end
9.オブジェクトの属性(プロパティ)を検証する「have_attributesマッチャ」(RSpec 3.1)
RSpec 3.1からはオブジェクトの属性(プロパティ)を検証する「have_attributes」マッチャが追加されています。
このマッチャを使うと、オブジェクトの属性を簡単に検証できます。
class Contact < ActiveRecord::Base
def name
[firstname, lastname].join(' ')
end
end
it '名前に関する属性が正しくセットされること' do
contact = Contact.new( firstname: 'Jane', lastname: 'Smith')
expect(contact).to have_attributes(firstname: 'Jane', lastname: 'Smith', name: 'Jane Smith')
end
まとめ
以上のようにRSpec 3から使えるようになった新機能を8つまとめてみました。
無理にすべての機能を使う必要はありませんが、テストコードを書きながら「あ、ここはあの新機能が使えるかも」と思ったら、試しに使ってみてください!
あわせて読みたい
既存のRSpec 2プロジェクトをRSpec 3にアップグレードする
必要最小限の努力で最大限実戦で使えるRSpecの知識を学ぶ
- 使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」
- 使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」
- 使えるRSpec入門・その3「ゼロからわかるモック(mock)を使ったテストの書き方」
- 使えるRSpec入門・その4「どんなブラウザ操作も自由自在!逆引きCapybara大辞典」
TDD形式でRSpecの基礎を学ぶ
- RSpecの入門とその一歩先へ ~RSpec 3バージョン~
- RSpec の入門とその一歩先へ、第2イテレーション ~RSpec 3バージョン~
- RSpec の入門とその一歩先へ、第3イテレーション ~RSpec 3バージョン~