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

# はじめに

RSpec 3が正式リリースされて2ヶ月ほど経過しました。(正式リリースは[2014年6月](https://github.com/rspec/rspec-core/blob/master/Changelog.md#300--2014-06-01))

ネットの情報を見ていると、これまでは「既存のテストケースを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](http://qiita.com/jnchito/items/3a590480ee291a70027c)

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

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

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

## 参考文献

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

- [New in RSpec 3: Composable Matchers](http://myronmars.to/n/dev-blog/2014/01/new-in-rspec-3-composable-matchers) ([日本語訳](http://nilp.hatenablog.com/entry/2014/02/28/215056))
- [Notable Changes in RSpec 3](http://myronmars.to/n/dev-blog/2014/05/notable-changes-in-rspec-3) ([日本語訳](http://nilp.hatenablog.com/entry/2014/05/28/003335))
- [RSpec 3.1 has been released!](http://myronmars.to/n/dev-blog/2014/09/rspec-3-1-has-been-released)

# 1.マッチャ同士を and や or で連結できる「マッチャ合成式」 (Compound Matcher Expressions)

RSpec 3では`and`または`or`を使って、マッチャを連結することができます。
これにより、「XがAかつBかつCであること」や「AまたはBまたはCであること」を一度に検証できるようになります。
使い方は以下のサンプルコードを見てください。

````ruby
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' にそれぞれ変更される」というようなケースでテストが書きやすくなります。


```ruby
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ではマッチャの引数にマッチャを渡せます・・・と言われてもピンとこないと思うのでサンプルコードを見てください。

````ruby
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`マッチャを配列として渡しています。

````ruby
# 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([...])`のように丸括弧で`[]`を囲むようにすると確実です。

````ruby
# エラー
# 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](http://myronmars.to/n/dev-blog/2014/01/new-in-rspec-3-composable-matchers) ([日本語訳](http://nilp.hatenablog.com/entry/2014/02/28/215056))

なお、`contain_exactly` や `output` はRSpec 3から登場した新しいマッチャです。詳しくは後述します。


# 3.テストコードや出力結果の可読性を向上させる「マッチャエイリアス」 (Matcher aliases)

RSpec 3では `a_[type of object]_[verb]ing` という形式のマッチャエイリアス(マッチャの別名)が数多く用意されています。

````ruby
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`を使っています。

他にも、コンポーザブルマッチャで使ったサンプルコードも次のように書き換えることができます。

````ruby
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")` よりも、<br> `change { x }.from a_string_starting_with("a")` の方が英文法的には自然
- テスト失敗時のメッセージも英文法的に正しくなる
  - `start_with` の場合<br>=> `expected result to have changed from start with "a", but did not change`
  - `a_string_starting_with` の場合<br>=> `expected result to have changed from a string starting with "a", but did not change`

## マッチャエイリアスは [verb]ing だけにすることも可能

マッチャエイリアスは `[verb]ing` だけにすることもできます。

````ruby
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](http://rubydoc.info/github/rspec/rspec-expectations/RSpec/Matchers)を見ると、"Also known as:"というような形でマッチャエイリアスが記載されています。

もしくは僕が個人的に作成したこちらのリストを参照してください。

- [List of alias matchers in RSpec 3](https://gist.github.com/JunichiIto/f603d3fbfcf99b914f86)

# 4.ハッシュや配列にも使えるようになった「matchマッチャ」

RSpec 2では`match`マッチャは文字列にしか使えませんでしたが、RSpec 3からはハッシュや配列の中身を検証する場合にも使えるようになりました。

````ruby
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`マッチャが追加されました。
下の例で示すように、配列の個数と各要素の同値性を検証したいときに使えます。(ただし順番は無視します)

````ruby
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`マッチャの引数には別のマッチャを渡してください。
たとえば以下のような感じです。

````ruby
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` マッチャを使ったサンプルコードを以下に示します。

````ruby
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)への出力結果を検証することができます。

````ruby
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」マッチャが追加されています。
このマッチャを使うと、オブジェクトの属性を簡単に検証できます。

```ruby
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 3へのアップグレードガイド](http://qiita.com/yujinakayama/items/a1d31b2caa35642e8e69)
- [既存のRailsプロジェクトをRSpec 3.0にアップグレードする際の注意点 ～RSpec 3は怖くないよ！～](http://qiita.com/jnchito/items/f86b57d27b44e6e7d1e9)

### 必要最小限の努力で最大限実戦で使えるRSpecの知識を学ぶ

- [使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」](http://qiita.com/jnchito/items/42193d066bd61c740612)
- [使えるRSpec入門・その2「使用頻度の高いマッチャを使いこなす」](http://qiita.com/jnchito/items/2e79a1abe7cd8214caa5)
- [使えるRSpec入門・その3「ゼロからわかるモック（mock）を使ったテストの書き方」](http://qiita.com/jnchito/items/640f17e124ab263a54dd)
- [使えるRSpec入門・その4「どんなブラウザ操作も自由自在！逆引きCapybara大辞典」](http://qiita.com/jnchito/items/607f956263c38a5fec24)

### TDD形式でRSpecの基礎を学ぶ

- [RSpecの入門とその一歩先へ ～RSpec 3バージョン～](http://qiita.com/jnchito/items/624f6d5023c279046a1c)
- [RSpec の入門とその一歩先へ、第2イテレーション ～RSpec 3バージョン～](http://qiita.com/jnchito/items/47f539ddca7dbc01acef)
- [RSpec の入門とその一歩先へ、第3イテレーション ～RSpec 3バージョン～](http://qiita.com/jnchito/items/b1215a5d548cac4c7de6)

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

- [Everyday Rails - RSpecによるRailsテスト入門 (電子書籍)](https://leanpub.com/everydayrailsrspec-jp)