RSpec の入門とその一歩先へ、第2イテレーション ～RSpec 3バージョン～


# はじめに

本記事は和田卓人さん(@t_wada)が書かれた有名なRSpec入門記事、「[RSpec の入門とその一歩先へ、第2イテレーション](http://d.hatena.ne.jp/t-wada/20100306/p1)」をRSpec 3バージョンとして書き直したものです。

詳しくは[第1イテレーション](http://qiita.com/jnchito/items/624f6d5023c279046a1c)に書いた説明を参照してください。


## 各イテレーション(RSpec 3バージョン)へのリンク

- [第1イテレーション](http://qiita.com/jnchito/items/624f6d5023c279046a1c)
- 第2イテレーション(本記事)
- [第3イテレーション](http://qiita.com/jnchito/items/b1215a5d548cac4c7de6)

## ソースコードのURL

- https://github.com/JunichiIto/rspec3-for-beginners/tree/end_of_iter2

## 本記事のライセンスについて

本記事は [クリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンス](http://creativecommons.org/licenses/by-sa/4.0/)で提供されています。

![banner](https://i.creativecommons.org/l/by-sa/4.0/88x31.png)

## 備考

- 本文やサンプルコードは極力オリジナルバージョンを踏襲します。
- 記述が古くなっている箇所は新しい記述方法に書き直します。
- 新しい記述方法や和田さんのオリジナル記事に書かれていない情報には **【注目！】** の印を付けて説明しています。
- [第1イテレーション](http://qiita.com/jnchito/items/624f6d5023c279046a1c)で説明した **【注目！】** 情報(shouldがexpect(...).toに変わった点等)は本記事では説明しません。

# 前回終了時点のコードと実行結果

前回終了時点でのコードを以下に記します。

[message_filter.rb](https://github.com/JunichiIto/rspec3-for-beginners/blob/end_of_iter1/message_filter.rb)

````ruby
class MessageFilter
  def initialize(word)
    @word = word
  end
  def detect?(text)
    text.include?(@word)
  end
end
````

[message_filter_spec.rb](https://github.com/JunichiIto/rspec3-for-beginners/blob/end_of_iter1/message_filter_spec.rb)

````ruby
require_relative 'message_filter'

describe MessageFilter, 'with argument "foo"' do
  subject { MessageFilter.new('foo') }
  it { is_expected.to be_detect('hello from foo') }
  it { is_expected.not_to be_detect('hello, world') }
end
````

実行してみましょう。

````bash
$ bundle exec rspec -fd message_filter_spec.rb

MessageFilter with argument "foo"
  should be detect "hello from foo"
  should not be detect "hello, world"

Finished in 0.00199 seconds (files took 0.08473 seconds to load)
2 examples, 0 failures
````

# 第2イテレーション

これまでの NG ワードフィルタは、NG ワードが一つしか使えませんでした。このままでは使い勝手がやや悪いですね。NG ワードを複数登録できるようにしてみます。


と、その前に前回 git を使っていた人は、前回のブランチをマージして新しいブランチを作成しておきましょう。

````bash
$ git checkout master
Switched to branch 'master'
$ git merge 1st
Updating cc2b2df..6740238
Fast-forward
 message_filter.rb      | 8 ++++++++
 message_filter_spec.rb | 7 +++++++
 2 files changed, 15 insertions(+)
 create mode 100644 message_filter.rb
$ git checkout -b 2nd
Switched to a new branch '2nd'
````

このイテレーションでも、大体見出し毎にコミットしています。

# spec ファイルの構造は比較的自由

では、第2イテレーション最初のテストを書きます。 RSpec では spec ファイルの構造は比較的自由です。なので、 describe ブロックも同じレベルに並べることができます。引数を増やしたテストの記述をまずは単純に下に増やしてみましょう。

message_filter_spec.rb

````diff
   it { is_expected.to be_detect('hello from foo') }
   it { is_expected.not_to be_detect('hello, world') }
 end
+
+describe MessageFilter, 'with argument "foo","bar"' do
+  subject { MessageFilter.new('foo', 'bar') }
+  it { is_expected.to be_detect('hello from bar') }
+end

````

実行してみましょう。

````bash
$ bundle exec rspec message_filter_spec.rb 
..F

Failures:

  1) MessageFilter with argument "foo","bar" 
     Failure/Error: subject { MessageFilter.new('foo', 'bar') }
     ArgumentError:
       wrong number of arguments (2 for 1)
     # ./message_filter.rb:2:in `initialize'
     # ./message_filter_spec.rb:10:in `new'
     # ./message_filter_spec.rb:10:in `block (2 levels) in <top (required)>'
     # ./message_filter_spec.rb:11:in `block (2 levels) in <top (required)>'

Finished in 0.00259 seconds (files took 0.08536 seconds to load)
3 examples, 1 failure

Failed examples:

rspec ./message_filter_spec.rb:11 # MessageFilter with argument "foo","bar" 
$ git commit -am 'Add spec for variable length argument'
[2nd a616565] Add spec for variable length argument
 1 file changed, 5 insertions(+)
````

[Commit](https://github.com/JunichiIto/rspec3-for-beginners/commit/a616565193515388488e64e7cbb7fbd10ead26a1)

引数ひとつを期待したところに引数がふたつ来たよというエラーになりました。想定どおりですね。

さて、どういう実装を書きましょうか。仮実装路線で行くか、それとも明白な実装路線で行くか。今回は背伸びして実装を一気にしてみましょう。

# 可変引数を使って実装してみる

明白な、というか、ちょっとベタな実装をしてみます。(この実装はイテレーション後半でリファクタリングします)

message_filter.rb

````diff
 class MessageFilter
-  def initialize(word)
-    @word = word
+  def initialize(*words)
+    @words = words
   end
   def detect?(text)
-    text.include?(@word)
+    @words.each do |w|
+      return true if text.include?(w)
+    end
+    false
   end
 end
````

実行してみましょう。

````bash
$ bundle exec rspec -fd message_filter_spec.rb

MessageFilter with argument "foo"
  should be detect "hello from foo"
  should not be detect "hello, world"

MessageFilter with argument "foo","bar"
  should be detect "hello from bar"

Finished in 0.00238 seconds (files took 0.08568 seconds to load)
3 examples, 0 failures
$ git commit -am 'Implement variable length argument'
[2nd 602e5d1] Implement variable length argument
 1 file changed, 6 insertions(+), 3 deletions(-)
````

[Commit](https://github.com/JunichiIto/rspec3-for-beginners/commit/602e5d15412c15db53bb177fdac7abd9158295da)

# テストを追加する

foo,bar というふたつの引数を取るフィルタは、 foo ひとつを引数に取る場合のテストも当然満たさなければならないですよね。ということで、まずは foo 一引数のテストをコピペして持ってきます。

message_filter_spec.rb

````diff
 describe MessageFilter, 'with argument "foo","bar"' do
   subject { MessageFilter.new('foo', 'bar') }
   it { is_expected.to be_detect('hello from bar') }
+  it { is_expected.to be_detect('hello from foo') }
+  it { is_expected.not_to be_detect('hello, world!') }
 end
````

実行してみましょう。

````bash
$ bundle exec rspec -fd message_filter_spec.rb

MessageFilter with argument "foo"
  should be detect "hello from foo"
  should not be detect "hello, world"

MessageFilter with argument "foo","bar"
  should be detect "hello from bar"
  should be detect "hello from foo"
  should not be detect "hello, world!"

Finished in 0.0025 seconds (files took 0.07999 seconds to load)
5 examples, 0 failures
$ git commit -am 'Add specs for variable length argument'
[2nd fb0a27e] Add specs for variable length argument
 1 file changed, 2 insertions(+)
````

[Commit](https://github.com/JunichiIto/rspec3-for-beginners/commit/fb0a27e5700aab36e8a19ce3e4ba1118efded56a)

# shared_examples を使ってテストの重複を排除する

一つ前のステップでコピペを行ったので、テストコードの重複が増えましたね。重複している code example は、 shared_examples という機能でまとめてみましょう。

shared_examples の内容は引数に渡した文字列をキーとして登録され、 it_behaves_like メソッドにその名前を使うことであたかもその場所に code example を書いたように動作します。

shared_examples と it_behaves_like は共に RSpec が提供するメソッドです。

**【注目！】**
RSpec 3では `share_examples_for` ではなく、 `shared_examples` もしくは `shared_examples_for` を使うようになりました。本記事では[Relishのサンプルコード](https://www.relishapp.com/rspec/rspec-core/v/3-0/docs/example-groups/shared-examples)に倣って`shared_examples`を使います。
`it_should_behave_like` は引き続き使えますが、RSpec 3では"should"というキーワードを使わなくなっているので、本記事では`it_behaves_like`を採用しています。


message_filter_spec.rb

````diff
 require_relative 'message_filter'
 
+shared_examples 'MessageFilter with argument "foo"' do
+  it { is_expected.to be_detect('hello from foo') }
+  it { is_expected.not_to be_detect('hello, world!') }
+end
+
 describe MessageFilter, 'with argument "foo"' do
   subject { MessageFilter.new('foo') }
-  it { is_expected.to be_detect('hello from foo') }
-  it { is_expected.not_to be_detect('hello, world') }
+  it_behaves_like 'MessageFilter with argument "foo"'
 end
 
 describe MessageFilter, 'with argument "foo","bar"' do
   subject { MessageFilter.new('foo', 'bar') }
   it { is_expected.to be_detect('hello from bar') }
-  it { is_expected.to be_detect('hello from foo') }
-  it { is_expected.not_to be_detect('hello, world!') }
+  it_behaves_like 'MessageFilter with argument "foo"'
 end
````

実行してみましょう。

````bash
$ bundle exec rspec -fd message_filter_spec.rb

MessageFilter with argument "foo"
  behaves like MessageFilter with argument "foo"
    should be detect "hello from foo"
    should not be detect "hello, world!"

MessageFilter with argument "foo","bar"
  should be detect "hello from bar"
  behaves like MessageFilter with argument "foo"
    should be detect "hello from foo"
    should not be detect "hello, world!"

Finished in 0.00277 seconds (files took 0.09055 seconds to load)
5 examples, 0 failures
$ git commit -am 'Use shared_examples'
[2nd 31c6476] Use shared_examples
 1 file changed, 7 insertions(+), 4 deletions(-)
````

[Commit](https://github.com/JunichiIto/rspec3-for-beginners/commit/31c64767f2ed0fc866c636df870abb2b9eb89d96)

テストとしての意味を保ったまま、コードは(比較的) DRY になりました。

今回は一つのファイルの中に shared_examples ブロックと it_behaves_like メソッドをまとめましたが、 shared_examples メソッドはその名のとおり複数のテストファイルで code example を共有するためにも使えます。というよりは、ファイルを越えて code example を積極的に共有するための機能です。

なぜ説明的な長めの文字列をキーに使用したかの説明も必要ですね。

先ほど書いたように、 it_behaves_like が書かれた場所と shared_examples が書かれている場所は別ファイルである可能性が高くなります。このため、 it_behaves_like が使われている場所から shared_examples の中身は「遠く」なりがちです。毎回 shared_examples の中身を見に行っているようでは共通化の意味がありません。つまり、 it_behaves_like の引数は十分に説明的で、先にある shared_examples の中身を見にいかなくても済むことが望ましいと考えています。以上が、私が shared_examples でかなり長い名前をつけた理由です。

**【注目！】**
オリジナルの記事では`share_as`についても言及されていますが、`share_as`はRSpec 3で削除されました。

# describe をネストする

今書いているテストは、 MessageFilter をつかう状況を二種類用意してテストしていると言えます。同じ対象に対する、別の状況のテストですね。 MessageFilter に対するテストであるという意図を示すために、ネストされた構造にテストコードを変更しましょう。今まで describe ブロックに二つ引数を渡していたところを、外側の describe ブロックの引数には MessageFilter 、内側の describe ブロックの引数には状況説明用の文字列を書くようにします。

message_filter_spec.rb

````diff
 require_relative 'message_filter'
 
-shared_examples 'MessageFilter with argument "foo"' do
-  it { is_expected.to be_detect('hello from foo') }
-  it { is_expected.not_to be_detect('hello, world!') }
+describe MessageFilter do
+  shared_examples 'MessageFilter with argument "foo"' do
+    it { is_expected.to be_detect('hello from foo') }
+    it { is_expected.not_to be_detect('hello, world!') }
+  end
+  describe 'with argument "foo"' do
+    subject { MessageFilter.new('foo') }
+    it_behaves_like 'MessageFilter with argument "foo"'
+  end
+  describe 'with argument "foo","bar"' do
+    subject { MessageFilter.new('foo', 'bar') }
+    it { is_expected.to be_detect('hello from bar') }
+    it_behaves_like 'MessageFilter with argument "foo"'
+  end
 end
-
-describe MessageFilter, 'with argument "foo"' do
-  subject { MessageFilter.new('foo') }
-  it_behaves_like 'MessageFilter with argument "foo"'
-end
-
-describe MessageFilter, 'with argument "foo","bar"' do
-  subject { MessageFilter.new('foo', 'bar') }
-  it { is_expected.to be_detect('hello from bar') }
-  it_behaves_like 'MessageFilter with argument "foo"'
-end
````

実行してみましょう。

````bash
$ bundle exec rspec -fd message_filter_spec.rb

MessageFilter
  with argument "foo"
    behaves like MessageFilter with argument "foo"
      should be detect "hello from foo"
      should not be detect "hello, world!"
  with argument "foo","bar"
    should be detect "hello from bar"
    behaves like MessageFilter with argument "foo"
      should be detect "hello from foo"
      should not be detect "hello, world!"

Finished in 0.00279 seconds (files took 0.09226 seconds to load)
5 examples, 0 failures
$ git commit -am 'Nest describe blocks'
[2nd 486431b] Nest describe blocks
 1 file changed, 17 insertions(+), 17 deletions(-)
 rewrite message_filter_spec.rb (93%)
````

[Commit](https://github.com/JunichiIto/rspec3-for-beginners/commit/486431b8e178224f5a89184e837d29da4dbd4d6a)

ネストしても RSpec が出力する仕様記述がほとんど変わっていないことが確認できると思います。

# 状況を記すには describe より context を好む

さきほど describe がネストできることを学びました。ところで、 RSpec は describe のエイリアスとして context というメソッドも用意しています。私は、対象を説明する時は describe, 状況を説明する時は context というように使い分けています。

message_filter_spec.rb

````diff
     it { is_expected.to be_detect('hello from foo') }
     it { is_expected.not_to be_detect('hello, world!') }
   end
-  describe 'with argument "foo"' do
+  context 'with argument "foo"' do
     subject { MessageFilter.new('foo') }
     it_behaves_like 'MessageFilter with argument "foo"'
   end
-  describe 'with argument "foo","bar"' do
+  context 'with argument "foo","bar"' do
     subject { MessageFilter.new('foo', 'bar') }
     it { is_expected.to be_detect('hello from bar') }
     it_behaves_like 'MessageFilter with argument "foo"'
````

実行してみましょう。

````bash
$ bundle exec rspec -fd message_filter_spec.rb

MessageFilter
  with argument "foo"
    behaves like MessageFilter with argument "foo"
      should be detect "hello from foo"
      should not be detect "hello, world!"
  with argument "foo","bar"
    should be detect "hello from bar"
    behaves like MessageFilter with argument "foo"
      should be detect "hello from foo"
      should not be detect "hello, world!"

Finished in 0.00291 seconds (files took 0.07985 seconds to load)
5 examples, 0 failures
$ git commit -am 'Use context instead of describe'
[2nd 777b266] Use context instead of describe
 1 file changed, 2 insertions(+), 2 deletions(-)
````

[Commit](https://github.com/JunichiIto/rspec3-for-beginners/commit/777b266ac10d76f7063a6e645e7237a0245c97ae)

# テストが整ったので再度実装のリファクタリングを行う

テストコードの方はだいぶリファクタリングできました。実装コードの方に目を向けてみましょう。最初に行った実装はあまりにも安易でしたね。テストがあるので、より綺麗なコードを追い求めることができます。 Enumerable#any? メソッドを使ってシンプルにしましょう。
Enumerable には素敵なメソッドが沢山あるので、覚えて損はありません。

message_filter.rb

````diff
     @words = words
   end
   def detect?(text)
-    @words.each do |w|
-      return true if text.include?(w)
-    end
-    false
+    @words.any?{|w| text.include?(w) }
   end
 end
````

実行してみましょう。

````bash
$ bundle exec rspec -fd message_filter_spec.rb

MessageFilter
  with argument "foo"
    behaves like MessageFilter with argument "foo"
      should be detect "hello from foo"
      should not be detect "hello, world!"
  with argument "foo","bar"
    should be detect "hello from bar"
    behaves like MessageFilter with argument "foo"
      should be detect "hello from foo"
      should not be detect "hello, world!"

Finished in 0.00274 seconds (files took 0.08211 seconds to load)
5 examples, 0 failures
$ git commit -am 'Refactoring'
[2nd 05d2bf1] Refactoring
 1 file changed, 1 insertion(+), 4 deletions(-)
````

[Commit](https://github.com/JunichiIto/rspec3-for-beginners/commit/05d2bf105bcb70439486f1fa09ab4f8cf00988a7)

# 第2イテレーション終了

このイテレーションではテスト/実装コードは最終的に以下のようになりました。

[message_filter.rb](https://github.com/JunichiIto/rspec3-for-beginners/blob/end_of_iter2/message_filter.rb)

````ruby
class MessageFilter
  def initialize(*words)
    @words = words
  end
  def detect?(text)
    @words.any?{|w| text.include?(w) }
  end
end
````

[message_filter_spec.rb](https://github.com/JunichiIto/rspec3-for-beginners/blob/end_of_iter2/message_filter_spec.rb)


````ruby
require_relative 'message_filter'

describe MessageFilter do
  shared_examples 'MessageFilter with argument "foo"' do
    it { is_expected.to be_detect('hello from foo') }
    it { is_expected.not_to be_detect('hello, world!') }
  end
  context 'with argument "foo"' do
    subject { MessageFilter.new('foo') }
    it_behaves_like 'MessageFilter with argument "foo"'
  end
  context 'with argument "foo","bar"' do
    subject { MessageFilter.new('foo', 'bar') }
    it { is_expected.to be_detect('hello from bar') }
    it_behaves_like 'MessageFilter with argument "foo"'
  end
end
````

さて、ここまでで第2イテレーションは終了です。タグを打って終わりにしましょう。

````bash
$ git tag -a -m 'end of 2nd iteration' end_of_iter2
````

[Tag](https://github.com/JunichiIto/rspec3-for-beginners/tree/end_of_iter2)

# このイテレーションで学んだこと

- spec ファイルの構造は比較的自由
- shared_examples で code example を共有できる
- describe はネストできる
- ものには describe, 状況には context


# 第3イテレーションに続きます

和田さんの記事と同様に、[第3イテレーション](http://d.hatena.ne.jp/t-wada/20100801/rspec_3rd_iter)の記事についてもRSpec 3バージョンを作成しました。
第1イテレーション、第2イテレーションと学習してきたなら、最後まで完走しましょう！

- [第3イテレーション(RSpec 3バージョン)](http://qiita.com/jnchito/items/b1215a5d548cac4c7de6)

## あわせて読みたい

既存の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 3の新機能について深く掘り下げたい方はこちらの記事をどうぞ。

- [今日から使える！RSpec 3で追加された8つの新機能](http://qiita.com/jnchito/items/a4a51852c2c678b57868)

こちらの記事ではRSpecの基本事項を実践的な視点で独自にまとめてみました。

- [使えるRSpec入門・その1「RSpecの基本的な構文や便利な機能を理解する」](http://qiita.com/jnchito/items/42193d066bd61c740612)

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

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

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

[Everyday Rails - RSpecによるRailsテスト入門](https://leanpub.com/everydayrailsrspec-jp)
![Everyday Rails](http://cdn-ak.f.st-hatena.com/images/fotolife/J/JunichiIto/20141024/20141024121511.png)

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

[RSpec 3.1に完全対応！「Everyday Rails - RSpecによるRailsテスト入門」をアップデートしました](http://blog.jnito.com/entry/2014/10/24/120921)