Help us understand the problem. What is going on with this article?

使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」

はじめに

みなさんこんにちは!
この記事は「必要最小限の努力で最大限実戦で使える知識を提供するRSpec入門記事」、略して「使えるRSpec入門」の第2回です。

今回はRSpecのマッチャについて説明していきます。
第1回と同様、今回も「最低限これだけは」という内容に絞り込んで説明します。
使用頻度の少ないマイナーなマッチャ(注:僕基準)については説明しません。

具体的な項目は以下の通りです。

  • マッチャとは何か
  • to / not_to / to_not
  • eq
  • be
  • be_xxx
  • be_truthy / be_falsey
  • change + from / to / by
  • 配列 + include
  • raise_error
  • be_within + of

これからRSpecを始める人はもちろん、何度かRSpecに触れて「うーん、RSpecってわけわからん」となっている人もこの記事で再入門してみると良いかもしれません。

そしてこの記事を最後まで読めば、次のテストコードの意味を理解できるようになっているはずです。

expect{ you.read_this_entry }.to change{ you.matcher_expert? }.from(be_falsey).to(be_truthy)

それでは始めましょう!

対象となる読者

  • 「RSpecってなんか怖そう」と思っているRSpec初心者の方
  • 「何度か使ってみたけど、RSpecってようわからん」と思っているRSpec経験者の方
  • Ruby関係のプロジェクトに放り込まれ、「RSpecでテストを書け」と言われて困惑している他のテストフレームワーク経験者の方

対象となるRSpecとRubyのバージョン

  • RSpec 3.1.0
  • Ruby 2.1.3

基本的に「素のRubyプログラム」をテストする場面を想定していますが、ときどきRailsの使用を前提としたサンプルコードも登場します。

今回説明 "しない" 内容

今回の記事ではRSpecのコア機能として提供される標準マッチャを対象とします。
以下のような内容は今回扱いません。

  • モック関連のマッチャ( expect(...).to receive(...) など)
  • Rails、Capybara関連のマッチャ( expect(...).to have_content(...) など)
  • カスタムマッチャ(自作マッチャ)の作り方

なお、モックに関しては第3回、Capybaraに関しては第4回の記事でそれぞれ説明する予定です。

第1回の記事はもう読みましたか?

第1回の記事では「RSpecの基本的な構文や便利な機能を理解する」というテーマでRSpecの基本を説明しました。
今回、「読者のみなさんは第1回の内容は理解できている」という前提で説明していくので、まだ読んでいない方は先に第1回を読んでおいてください。

そもそもマッチャって何?

「さっきからマッチャ、マッチャって言ってるけど、マッチャってなんやねん?」と思っている方も多いかもしれません。

マッチャ(matcher)は「期待値と実際の値を比較して、一致した(もしくは一致しなかった)という結果を返すオブジェクト」のことです。

簡単なサンプルコードを使って、マッチャを確認してみましょう。

expect(1 + 2).to eq 3
expect([1, 2, 3]).to include 2

上のサンプルコードでいうと、 eqinclude がマッチャにあたります。
乱暴に言ってしまえば、「 expect(...).to xxxto の直後に出てくるやつ(xxx の部分)がマッチャ」です。

マッチャは自分自身に定義されている検証ルールに従って、実際の値(1 + 2[1, 2, 3])と期待値(32)を比較し、ルールに合致しているかどうかを判断します。

to / not_to / to_not

今さら説明するのもおかしいかもしれませんが、「~であること」を期待する場合は to を使います。

expect(1 + 2).to eq 3

反対に「~ではないこと」を期待する場合は not_to もしくは to_not を使います。

expect(1 + 2).not_to eq 1
# または
expect(1 + 2).to_not eq 1

ちなみに、「 expect(...).to xxxto の直後に出てくるやつがマッチャ」なので、tonot_to 自体はマッチャではありません。
tonot_to はマッチャの実行結果を受け取って、テストをパスさせるか否かを判断するRSpecのメソッドです。

参考: not_toto_not 、どちらを使うべき?
RSpecの公式ドキュメントによると、 「読みやすい方を使えば良い」 と書いてあるだけで、どちらか一方を推奨しているわけではありません。
ネイティブスピーカーであれば文脈によって「読みやすい方」を選択できるのでしょうが、普通の日本人はそのニュアンスを識別するのは難しいと思います。(僕もわかりません)
というわけで、 「別にどっちでもいい」が正解 になります。

ただ、RSpecの公式ドキュメントのサンプルコードは not_to をよく使っているので、迷ったときはとりあえず not_to を使ってみてはどうでしょうか?

eq

これまた基本中の基本ですが、期待値と実際の値が「等しい」かどうかを検証する場合は eq を使います。

expect(1 + 2).to eq 3

RSpecでテストを書く場合、大半はこの eq を使ってテストを書くはずです。

be

be は等号・不等号と組み合わせて、値の大小を検証するときによく使われるマッチャです。
たとえば以下のコードは「1 + 2 が 3 以上であること」を検証しています。

expect(1 + 2).to be >= 3

ちょっとややこしい話:be と eq の違い

beeq は似たような用途で使うこともできます。
ただし、微妙に動きが異なるので注意が必要です。
この項の話はちょっとややこしいので、初心者の方はさらっと読み流す程度でも構いません。

では本題です。
be は等号・不等号なしで次のように書くこともできます。

message = 'Hello'
expect([message].first).to be message

等号・不等号なしで書いた場合は、2つの値が同一のインスタンスかどうかを検証します。
つまり、equal? メソッドの結果が true かどうかを検証していることになります。

message = 'Hello'
# [message].first.equal? message を検証している
expect([message].first).to be message

同じ値でもインスタンスが異なる場合はテストは失敗します。

message_1 = 'Hello'
message_2 = 'Hello'
# message_1 と message_2 は異なるインスタンスなのでテストが失敗する
expect([message_1].first).to be message_2

一方、be の代わりに eq を使うと == を使って比較するので、先ほどのテストはパスします。

message_1 = 'Hello'
message_2 = 'Hello'
# message_1 == message_2 の結果は真になるのでテストはパスする
expect([message_1].first).to eq message_2

同一インスタンスかどうかを検証する機会はあまり多くないと思うので、大半のテストケースでは be ではなく eq を使えばOKでしょう。

ただし、true / false / nil や、整数値、シンボルは特殊で、「同じ値であれば同じインスタンス」になるため、be を使ってもテストはパスします(eq でもパスします)。

# true / false / nil はいつでも同じインスタンス
expect(true).to be true
expect(false).to be false
expect(nil).to be nil

# 整数値やシンボルは、同じ値はいつでも同じインスタンス
expect(1 + 1).to be 2
expect(:foo).to be :foo

以上の議論をまとめると次のようになります。

  • expect(A).to be BA.equal?(B) が真であれば(つまり、同一インスタンスであれば)パスする
  • expect(A).to eq BA == B が真であれば(つまり、同値であれば)パスする
  • true やシンボルのように、「同じ値は常に同じインスタンス」になる場合は eqbe を入れ替えても同じ結果になる

少しややこしいルールですが、頭の片隅に置いておくといつか役に立つときがくるかもしれません。

be_xxx (predicateマッチャ)

RSpecで特徴的なのが、 empty? のようにメソッド名が「?」で終わり、戻り値が true / false になるメソッドを be_empty のような形式で検証できることです。

たとえばこんな感じです。

expect([]).to be_empty

このコードは意味的に次のコードと同じになります。

expect([].empty?).to be true
# または
expect([].empty?).to eq true

他にも、Railsのmodelにバリデーションエラーが発生していないことを検証する場合には、次のように書くことができます。

user = User.new(name: 'Tom', email: 'tom@example.com')
expect(user).to be_valid # user.valid? が true になればパスする

be_xxx のような predicateマッチャを使う利点は、テストコードが自然な英文っぽく読める点です。
("expect user to be valid"のように)

一方で、「黒魔術的だし、RSpecの流儀を知らない人がテストコードを読んだときに理解しづらい」と感じる人もいて、結構好き嫌いの分かれる記法でもあります。

とはいえ、それほど複雑なルールではないと思いますし、(英文として)読みやすくなるので、僕個人は積極的に活用していった方が良いと考えています。

余談:"predicate"ってどういう意味?
ちなみに predicate(発音 ≒ プレディカット)は「述語の」という意味の形容詞です。
なので、"predicate matcher"を(むりやり)訳すと「述語的マッチャ」となります。
確かに、英文法的には「 "to" のうしろに動詞として持ってこれるから "be_empty" は述語になっている」と考えられなくもないです。
・・・が、テストを書くときはそんなことまで考える必要はたぶんないと思います(苦笑)。

be_truthy / be_falsey

「?」で終わらないが、戻り値として true / false を返すメソッドは be_truthy / be_falsey というマッチャで検証することができます。

たとえば、Railsのmodelで save メソッドを呼ぶと、保存に成功した場合は true 、失敗した場合は false が返ってきます。
なので、be_truthy / be_falsey を使って次のようなテストが書けます。

class User < ActiveRecord::Base
  validates :name, :email, presence: true
end
# 必須項目が入力されていないので保存できない(結果はfalse)
user = User.new
expect(user.save).to be_falsey 

# 必須項目が入力されているので保存できる(結果はtrue)
user.name = 'Tom'
user.email = 'tom@example.com'
expect(user.save).to be_truthy

be_truthy / be_falsey と be true / be false との違い

真偽値の評価に関するRubyの言語仕様はご存知でしょうか?
Rubyでは「false または nil であれば偽、それ以外は全て真」と評価します。

be_truthy / be_falsey を使うと、その仕様にあわせて戻り値の真偽を検証してくれます。
つまり、「trueっぽい値」または「falseっぽい値」かどうかを検証してくれるのが be_truthy / be_falsey です。

一方、 be true / be false (または eq true / eq false) を使うと true もしくは false であることを厳密に検証するので、それ以外の値を渡されるとテストが失敗します。

言葉で説明するよりも具体的なコードを見てもらった方がわかりやすいかもしれません。

# どちらもパスする
expect(1).to be_truthy
expect(nil).to be_falsey

# どちらも失敗する
expect(1).to be true
expect(nil).to be false

# be の代わりに eq を使った場合も同様に失敗する
expect(1).to eq true
expect(nil).to eq false

というわけで、RSpecで真偽値を確認するテストを書く場合は、何か特別な事情がない限り be_truthy / be_falsey を使った方が「Rubyらしい真偽値のテスト」ができます。

change + from / to / by

これまでに述べてきたマッチャに比べると少し構文が難しくなりますが、個人的に使用頻度が高い change マッチャの説明をします。

サンプルコードを見ながら使い方を確認してみましょう。
たとえば、以下のようなテストがあったとします。

# popメソッドを呼ぶと配列の要素が減少することをテストする
x = [1, 2, 3]
expect(x.size).to eq 3
x.pop
expect(x.size).to eq 2

上のテストは change マッチャを使うと次のように書き換えることができます。

x = [1, 2, 3]
expect{ x.pop }.to change{ x.size }.from(3).to(2)

注意してほしいのは expect{ x.pop }.to のように、丸括弧ではなく中括弧を使っている点です。
これはRubyの文法的にはブロックを expect に渡しています。
同様に、 change{ x.size } の部分でも中括弧を使っているので、ここもブロックを渡しています。

change マッチャを使ったテストコードは次のように読んでください。

expect{ X }.to change{ Y }.from(A).to(B) = 「X すると Y が A から B に変わることを期待する」

それから、change のバリエーションとして by を使った書き方もあります。
by を使うと「(元の個数はともかく)1個減ること」を検証できます。

x = [1, 2, 3]
expect{ x.pop }.to change{ x.size }.by(-1)

もちろん、減る場合だけでなく増える場合も検証できます。

x = [1, 2, 3]
expect{ x.push(10) }.to change{ x.size }.by(1)

change を使った応用例

change マッチャを使う場合は、上で示したコードよりももうちょっと複雑な処理を検証することが多いです。

例として、Railsで「userを削除すると、userが書いたblogも削除されること」を検証してみましょう。

class User < ActiveRecord::Base
  # dependent: :destroy を付けたので、userを削除するとblogも削除される
  has_many :blogs, dependent: :destroy
end

class Blog < ActiveRecord::Base
  belongs_to :user
end
it 'userを削除すると、userが書いたblogも削除されること' do
  user = User.create(name: 'Tom', email: 'tom@example.com')
  # user が blog を書いたことにする
  user.blogs.create(title: 'RSpec必勝法', content: 'あとで書く')

  expect{ user.destroy }.to change{ Blog.count }.by(-1)
end

このように change マッチャを使うと、「Xに対するある操作が、一見無関係なYに影響を与える」といった検証内容を簡潔に表現することができます。

ときどき使うマッチャあれこれ

さて、モックやRSpec Railsなどの特殊な用途のマッチャを除けば、ここまでに紹介したマッチャで標準的なテストはほとんどカバーできると思います。

続いてここからは「そこまで使用頻度は高くないけど、知っておくと便利かも」というマッチャをいくつか紹介していきます。

配列 + include

include マッチャを使うと、「配列に~が含まれていること」を検証することができます。

x = [1, 2, 3]
# 1が含まれていることを検証する
expect(x).to include 1
# 1と3が含まれていることを検証する
expect(x).to include 1, 3

他にも include はハッシュや文字列に対しても使うことができます。
また、 contain_exactly というマッチャを使うと、配列の順番は無視して要素の個数や内容を検証することができます。

・・・などなど、バリエーションはいろいろあるのですが、個人的には「配列 + include」以外ほとんど使ってないので、ここでは説明を割愛します。
気になる方はRSpecの公式ドキュメントを読んでみてください。

raise_error

「エラーが起きること」を検証する場合は、 raise_error マッチャを使うことができます。

たとえば、「0で除算するとエラーが起きること」は次のようにテストできます。

expect{ 1 / 0 }.to raise_error ZeroDivisionError

change マッチャと同様、expect にはブロック(中括弧)を渡している点に注意してください。

続けてもう少し実践的なサンプルコードを見てみましょう。
ここでは自作クラスで明示的にエラーを返すようにしたメソッドをテストしています。

class ShoppingCart
  def initialize
    @items = []
  end
  def add(item)
    raise 'Item is nil.' if item.nil?
    @items << item
  end
end
it 'nilを追加するとエラーが発生すること' do
  cart = ShoppingCart.new
  expect{ cart.add nil }.to raise_error 'Item is nil.'
end

上の例では raise_error の引数としてエラーメッセージ(文字列)を渡しています。
このように raise_error にはエラーのクラスやエラーメッセージ(文字列)など、いろいろな引数を渡すことができます。
詳しい使い方はRSpecの公式ドキュメントを読んでみてください。

be_within + of

プログラムのテストというと実行結果がバシッとひとつの値に定まることが多いですが、ときおり「ある程度のゆらぎ」を許容しなければならないケースもあります。
たとえば、ランダムに「あたり」を出すおみくじプログラムなどがそうです。

その場合は be_within(Y).of(X) で「数値 X がプラスマイナス Y の範囲内に収まっていること」を検証することができます。

実際に簡易的なおみくじプログラムを作って、テストコードを書いてみましょう。

class Lottery
  KUJI = %w(あたり はずれ はずれ はずれ)
  def initialize
    @result = KUJI.sample
  end
  def win?
    @result == 'あたり'
  end
  def self.generate_results(count)
    Array.new(count){ self.new }
  end
end
it '当選確率が約25%になっていること' do
  results = Lottery.generate_results(10000)
  win_count = results.count(&:win?)
  probability = win_count.to_f / 10000 * 100

  expect(probability).to be_within(1.0).of(25.0)
end

テストコードの probability が当選確率です。
KUJI = %w(あたり はずれ はずれ はずれ) なので、「あたり」を引く確率は 1/4、つまり25%になるはずです。
しかし、「あたり」はランダムに発生する(sample メソッドを使っている)ので25%ピッタリになる可能性はほとんどありません。
なので、 be_within(1.0).of(25.0) で前後1%のゆらぎを許容しています。
つまり、ここでは probability が24から26の間に収まっていればテストはパスします。

参考:「results.count(&:win?) って何やってるの?」と思った方へ
results.count(&:win?)results.count{|r| r.win? } と同じ意味です。
このあたりのテクニックについてはこちらのQiita記事を参照してください。

まだまだいっぱいあるよ!!(僕はほとんど使わないけど・・・)

RSpecが標準で用意しているマッチャや応用的な使い方はほかにもたくさんあります。
しかし、数年間RSpecでテストを書いてきましたが、僕個人は今回紹介したマッチャ以外はほとんど使ったことがありません。

RSpec上級者の方が読むと、「これもあれも紹介すべきじゃないか」と感じるかもしれませんが、今回は「RSpec初心者の方が、必要最小限の努力で最大限実戦で使える知識を提供する」というスタンスで書いているので、極力紹介する内容を削るようにしました。

とはいえ、自分の引き出しはたくさん持っておくに越したことはありません。

今回紹介したような基本的なマッチャの使い方に慣れてきたら、RSpecの公式ドキュメントを読んでより高度なマッチャの使い方を研究してみてください。

また、RSpec 3から導入されたマッチャ関連の新機能を詳しく知りたい場合は以下のQiita記事が役に立つと思います。

まとめ

というわけで、今回は「使用頻度の高そうなマッチャ」をピックアップして一通り説明してみました。
これぐらいのマッチャを使いこなせることができれば、普通のテストは問題なく書くことができると思います。

あとは必要に応じて公式ドキュメントを読んで知識を足していったり、ネットを検索して応用的な使い方を調べたりすれば、だんだんと上級者に近づいていくと思います。
最初から「全部覚えなきゃ」と考える必要はないので、気負わずRSpecの世界に飛び込んでみてください。

あ、そういえば冒頭に書いたテストコードは覚えていますか?

expect{ you.read_this_entry }.to change{ you.matcher_expert? }.from(be_falsey).to(be_truthy)

ここまで読んでくれたみなさんはもう理解できますよね。

you.read_this_entry すれば you.matcher_expert?false から true になることを期待する」

つまり

「あなたはこの記事を読めばマッチャを使いこなせるようになっているはず」

というテストコードを書いていたのでした。
このテストはきっとパスしていると思います!

さて、この入門記事シリーズはもう少し続く予定です。
第3回では「モックの使い方」を、第4回では「CapybaraのDSL」を取り上げるつもりです。

「続きが読みたい!」という方はこの記事をストックしてもらうと、新しい記事を投稿したときに通知メールを送りますので、どうぞよろしくお願いします!

【2014.11.21 追記】第3回の記事を公開しました!

【2015.01.02 追記】第4回の記事を公開しました!

おまけ

せっかくなので、 「あなたはこの記事を読めばマッチャを使いこなせるようになっているはず」 を実行可能なRubyのクラスとRSpecのテストコードとして表現してみました。

第1回の記事とあわせて読んでくれた方は、きっとテストコードの意味がわかるはずです。

class You
  def read_this_entry
    @matcher_expert = true
  end
  def matcher_expert?
    @matcher_expert
  end
end
RSpec.describe You do
  describe '#read_this_entry' do
    let(:you) { You.new }
    it 'この記事を読むとマッチャを使いこなせるようになっていること' do
      expect{ you.read_this_entry }.to \
        change{ you.matcher_expert? }.from(be_falsey).to(be_truthy)
    end
  end
end

ちなみに from(be_falsey).to(be_truthy) の部分は、RSpec 3から導入された「コンポーザブルマッチャ」という機能をさりげなく(?)使っています。

「コンポーザブルマッチャ」については以下の記事で詳しく説明しています。

あわせて読みたい

RSpecの公式ドキュメント

今回紹介した内容は、以下の公式ドキュメントを僕なりに要約したような内容になっています。
詳しい仕様を確認したい方は公式ドキュメントを読んでみてください。

RSpec Expectations 3.1

TDDのプロセスを重視したRSpecの入門記事

また、RSpec関連の入門記事としてはこちらの記事もオススメです。

「RSpecの入門とその一歩先へ ~RSpec 3バージョン~」

PR: RSpec 3.1に対応した「Everyday Rails - RSpecによるRailsテスト入門」が発売中です

僕が翻訳者として携わった 「Everyday Rails - RSpecによるRailsテスト入門」 という電子書籍が発売中です。

Railsアプリケーションを開発している方で、実践的なテストの書き方を学んでみたいという人には最適な一冊です。
現行バージョンはRSpec 3.1に対応しています。
一度購入すれば将来無料でアップデート版をダウンロードできるのも本書の特徴の一つです。
よかったらぜひ読んでみてください!

Everyday Rails - RSpecによるRailsテスト入門
Everyday Rails

本書の内容に関する詳しい情報はこちらのブログをどうぞ。

RSpec 3.1に完全対応!「Everyday Rails - RSpecによるRailsテスト入門」をアップデートしました

jnchito
SIer、社内SEを経て、ソニックガーデンに合流したプログラマ。 「プロを目指す人のためのRuby入門」の著者。 http://gihyo.jp/book/2017/978-4-7741-9397-7 および「Everyday Rails - RSpecによるRailsテスト入門」の翻訳者。 https://leanpub.com/everydayrailsrspec-jp
https://blog.jnito.com/
sonicgarden
「お客様に無駄遣いをさせない受託開発」と「習慣を変えるソフトウェアのサービス」に取り組んでいるソフトウェア企業
http://www.sonicgarden.jp
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした