Ruby
RSpec

今日から使える!RSpec 3で追加された8つの新機能

More than 3 years have passed since last update.

はじめに

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

本記事で紹介する新機能の一覧

本記事では以下のような新機能を紹介します。

  1. マッチャ同士を andor で連結できる 「マッチャ合成式」
  2. マッチャの引数に別のマッチャが渡せる 「コンポーザブルマッチャ」
  3. テストコードや出力結果の可読性を向上させる 「マッチャエイリアス」
  4. ハッシュや配列にも使えるようになった matchマッチャ」
  5. (順番は無視して)配列の中身を検証する contain_exactlyマッチャ」
  6. コレクションの全要素がtrueになることを検証する allマッチャ」
  7. exclusiveモードが追加された be_betweenマッチャ」
  8. 標準出力や標準エラー出力の出力結果を検証する outputマッチャ」
  9. オブジェクトの属性(プロパティ)を検証する have_attributesマッチャ」(RSpec 3.1)

参考文献

本記事は以下の参考文献を僕なりにまとめたものです。
サンプルコード等はこちらから拝借しています。

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_exactlyoutput は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_arraycontain_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の知識を学ぶ

TDD形式でRSpecの基礎を学ぶ

RSpecでRailsをテストする方法を学ぶ