RSpec の入門とその一歩先へ


クリエイティブ・コモンズ・ライセンス
和田 卓人(@t_wada) 作『RSpec の入門とその一歩先へ』はクリエイティブ・コモンズ 表示 - 継承 4.0 国際 ライセンスで提供されています。


東京 Ruby 会議 03 の RSpec ワークショップの資料です。このワークショップでは参加者の方に「写経」(コードを書き写すこと)をして貰い、TDD/BDD と RSpec を同時に学べるように都度説明を入れるかたちで行いました。


第2イテレーションも書きました。続きに興味ある方はご覧下さい

(更新) 第3イテレーションも書きました。続きに興味ある方はご覧下さい

1st iteration

favotter の みたいな NG ワードのフィルタリング機能を RSpec で作りましょう。まずは NG ワードの検出機能を作成します。

このイテレーションでは最初ベタな形のテストコードと実装を書き、だんだんとそのコードを洗練させてゆきます。


message_filter_spec.rb を作成

まずはこれから育てていく spec ファイルを作成します。

なお、このエントリのコードは diff 風の記法で書かれています。 "+" が追加された行、 "-" が削除された行です。


message_filter_spec.rb

+require 'rubygems'
+require 'spec'
+
+describe MessageFilter do
+end

git に登録

git がある人は、これから書くコードを git で管理しましょう。各ステップ毎に commit を行いつつ進めましょう。
(私は emacs から git を使うので明示的にコマンドラインでコミットを行っていませんが、このエントリの所々に出てくる sha1 は私が資料作成した時の sha1 です)

$ ls
message_filter_spec.rb
$ git init
Initialized empty Git repository in /home/takuto/work/git-sandbox/event/tork03/.git/
$ git add .
$ git commit -m 'initial'
Created initial commit 2bc2345: initial
 1 files changed, 5 insertions(+), 0 deletions(-)
 create mode 100644 message_filter_spec.rb
$ git checkout -b 1st
Switched to a new branch "1st"


実行してみましょう

$ spec message_filter_spec.rb 
./message_filter_spec.rb:4: uninitialized constant MessageFilter (NameError)
	from /home/takuto/app/gems/1.8/gems/rspec-1.3.0/lib/spec/runner/example_group_runner.rb:15:in `load'
	from /home/takuto/app/gems/1.8/gems/rspec-1.3.0/lib/spec/runner/example_group_runner.rb:15:in `load_files'
	from /home/takuto/app/gems/1.8/gems/rspec-1.3.0/lib/spec/runner/example_group_runner.rb:14:in `each'
	from /home/takuto/app/gems/1.8/gems/rspec-1.3.0/lib/spec/runner/example_group_runner.rb:14:in `load_files'
	from /home/takuto/app/gems/1.8/gems/rspec-1.3.0/lib/spec/runner/options.rb:133:in `run_examples'
	from /home/takuto/app/gems/1.8/gems/rspec-1.3.0/lib/spec/runner/command_line.rb:9:in `run'
	from /home/takuto/app/gems/1.8/gems/rspec-1.3.0/bin/spec:5
	from /home/takuto/app/gems/1.8/bin/spec:19:in `load'
	from /home/takuto/app/gems/1.8/bin/spec:19
$ 

RSpec が泣いているので、 message_filter.rb を作成しましょう

commit 54876c311b65dee84e530aec8add4212ce0e5f85


message_filter.rb

+class MessageFilter
+end

message_filter_spec.rb

 require 'rubygems'
 require 'spec'
+require 'message_filter'
 
 describe MessageFilter do
 end

実行してみましょう

$ spec message_filter_spec.rb 


Finished in 0.003614 seconds

0 examples, 0 failures
$ 

最初のテスト (code example) を書きましょう

commit d67ccc04a2991b14dae66e4cecab9de5b0dc351c

非常にベタな書き方ですが、最初のテスト (code example) を書きます。このイテレーションの後半で、テストの書き方も洗練させていきます。


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 describe MessageFilter do
+  it 'should detect message with NG word' do
+    filter = MessageFilter.new('foo')
+    filter.detect?('hello from foo').should == true
+  end
 end


実行してみましょう

$ spec message_filter_spec.rb 
F

1)
ArgumentError in 'MessageFilter should detect message with NG word'
wrong number of arguments (1 for 0)
./message_filter_spec.rb:7:in `initialize'
./message_filter_spec.rb:7:in `new'
./message_filter_spec.rb:7:

Finished in 0.003438 seconds

1 example, 1 failure
$ 

コンストラクタの引数の数が不正であると言われました。それはそうですね、コンストラクタを作成します。



コンストラクタ作成

commit 5a8d1dc52e190d6e50c5df9d56818611d8b08c44

message_filter.rb

 class MessageFilter
+  def initialize(word)
+    @word = word
+  end
 end

実行してみましょう

$ spec message_filter_spec.rb 
F

1)
NoMethodError in 'MessageFilter should detect message with NG word'
undefined method `detect?' for #<MessageFilter:0xb7ab05d8 @word="foo">
./message_filter_spec.rb:8:

Finished in 0.003369 seconds

1 example, 1 failure
$ 

テストはまだ落ちています。 detect? というメソッドが無いよと言われました。無いですね。作りましょう。



detect? メソッド作成

commit 57e84d6a9f26a45c3cec25a520100e25e3d119b8

空で良いので、メソッドを作成します。


message_filter.rb

 class MessageFilter
   def initialize(word)
     @word = word
   end
+  def detect?(text)
+  end
 end

実行してみましょう

$ spec message_filter_spec.rb 
F

1)
'MessageFilter should detect message with NG word' FAILED
expected: true,
     got: nil (using ==)
./message_filter_spec.rb:8:

Finished in 0.003675 seconds

1 example, 1 failure
$ 

true が返ってきてほしいところに nil が返ってきました。メソッドの中身が空だからですね。ではこのテストを通すもっとも簡単な実装はどうなるでしょうか。ここに TDD のトリックがあります。それが、「仮実装 (fake it)」です。



仮実装 (fake it)


commit e53b370ee40947870c152e4dd43bbc6ee4b4e252

先ほどのテストを通すためのもっとも単純な実装はどうなるでしょうか? 書いてみましょう。

message_filter.rb

 class MessageFilter
     @word = word
   end
   def detect?(text)
+    true
   end
 end

こ れ は ひ ど い !! しかし、これが TDD の「仮実装」です。


実行してみましょう

$ spec message_filter_spec.rb 
.

Finished in 0.002809 seconds

1 example, 0 failures
$ 

メソッドから true を返すベタな実装を行ったのでテストが通るのは当たり前ですね。こんな行為に何か意味があるのでしょうか?


仮実装とは、テストのテスト、と考えることが出来ます。例えば今回の例で、 true を返すという絶対テストが通るだろうという実装コードを書いても、テストが失敗したらどうでしょうか? それは、テストコードの方にこそバグが潜んでいることを示唆しています。仮実装で成功しないテストは、本実装でも成功しないでしょう。本実装でもテストが通らなかったときに、なぜテストが通らないのか本実装を長い時間デバッグした結果、テストコードが間違っていたのでは目も当てられません。



三角測量

commit c2e9fc3aba93ab49e6138b7402c4772cb0a08674


しかし、このままでは実装はいつまでも安易過ぎるものになってしまうので、別のデータを使ったテストを足しましょう。これを TDD では「三角測量(triangulation)」といいます。


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 describe MessageFilter do
   it 'should detect message with NG word' do
     filter = MessageFilter.new('foo')
     filter.detect?('hello from foo').should == true
   end
+  it 'should not detect message without NG word' do
+    filter = MessageFilter.new('foo')
+    filter.detect?('hello, world!').should == false
+  end
 end


実行してみましょう

$ spec message_filter_spec.rb 
.F

1)
'MessageFilter should not detect message without NG word' FAILED
expected: false,
     got: true (using ==)
./message_filter_spec.rb:12:

Finished in 0.00399 seconds

2 examples, 1 failure
$ 

「仮実装」で書いたコードはあっという間に破綻しました。そろそろきちんと実装しないといけないですね。



明白な実装

commit ecd31f317c758aad94b6dae6d2bf1453e067c758

message_filter.rb

 class MessageFilter
   def initialize(word)
     @word = word
   end
   def detect?(text)
-    true
+    text.include?(@word)
   end
 end


実行してみましょう

$ spec message_filter_spec.rb 
..

Finished in 0.002986 seconds

2 examples, 0 failures
$ 

実装が見えているときは、三角測量を介さずにそのまま仮実装を変更して実装してもかまいません。これを「明白な実装 (obvious implementation)」といいます。


大事なのは自分の不安をコントロールすることです。 TDD では、テストと実装両方に自信がある時は「明白な実装」、一歩一歩進めたい、つまりテストをテストして、そのあとで実装をテストしたい時は「仮実装」と「三角測量」の組み合わせを使います。



タグを打つ

きりのいいところまできたので、一度タグを打ちます。

$ git tag -a -m '1st iteration spec refactoring point' iter1_spec_refactoring
$ git tag
iter1_spec_refactoring
$ 

テストのリファクタリング

さて、テストが全部通っているので、実装クラスのリファクタリングを行いましょう。リファクタリングとは、コードに重複があったり、無駄がある場合に、テストが通ったままで実装を綺麗にしていくことです。実装にコードの重複や無駄はあるでしょうか? 現時点ではリファクタリングの余地がないほどシンプルですね。ではテストコードはどうでしょうか? …かなり重複が見られますね。テストを書いたすぐ後のタイミングで、テストコードの重複も積極的に排除していこう、というのが最近の考え方です。テストの「リファクタリング」というと厳密にはもっと難しく、タイミングが遅れるほど困難なものですが、テスト実装直後では自分の頭にもテスト設計が残っているでしょうし、このタイミングでは大胆に行動できます。では重複を排除していきましょう。



before メソッドの抽出

commit 4a3a7c895965e7f719a7b7b7a07bf8cd23fe1b2b


before メソッドを作成し、filter のインスタンス作成部分をそこに移動します。 before とは、 xUnit でいうと setUp に相当します。before メソッドは各テストの実行前に毎回実行されますので、重複部を before に書くことでテストコードの重複を排除することができます。


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 describe MessageFilter do
+  before(:each) do
+    @filter = MessageFilter.new('foo')
+  end
   it 'should detect message with NG word' do
-    filter = MessageFilter.new('foo')
-    filter.detect?('hello from foo').should == true
+    @filter.detect?('hello from foo').should == true
   end
   it 'should not detect message without NG word' do
-    filter = MessageFilter.new('foo')
-    filter.detect?('hello, world!').should == false
+    @filter.detect?('hello, world!').should == false
   end
 end


実行してみましょう

$ spec message_filter_spec.rb 
..

Finished in 0.003297 seconds

2 examples, 0 failures
$ 

大丈夫そうですね、本当でしょうか?
(テストのリファクタリング、 mutation testing について、あとでかく?)



:each は不要

commit 2e71d40efc324671f18bc7940d9e9696507b2384

ちなみに、 :each はデフォルト扱いなので、明示的に書く必要はありません。(before(:all) について、あとで書く?)


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 describe MessageFilter do
-  before(:each) do
+  before do
     @filter = MessageFilter.new('foo')
   end
   it 'should detect message with NG word' do
     @filter.detect?('hello from foo').should == true
   end
   it 'should not detect message without NG word' do
     @filter.detect?('hello, world!').should == false
   end
 end


実行してみましょう

$ spec message_filter_spec.rb 
..

Finished in 0.003297 seconds

2 examples, 0 failures
$ 

be_[predicate] マッチャー

commit fac3a62ae1a25a3abe33514383156f9673485e5e


"XXX?.should == true" や "XXX.should be_true" という記述は、 be_[predicate] マッチャーという書き方に変換することができます。こういう書き方をすることでドキュメントとしてのテストコードの意味を高め、かつ記述自体も簡潔にすることが出来ます。


"hoge.fuga?.should == true" は "hoge.should be_fuga" と書き直すことができます。 be_fuga というメソッドは当然存在しませんが、 RSpec が method_missing を使い、テスト用のメソッドであると解釈してくれます。これもメタプログラミングの例と言うことも出来ます。



message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 describe MessageFilter do
   before do
     @filter = MessageFilter.new('foo')
   end
   it 'should detect message with NG word' do
-    @filter.detect?('hello from foo').should == true
+    @filter.should be_detect('hello from foo')
   end
   it 'should not detect message without NG word' do
-    @filter.detect?('hello, world!').should == false
+    @filter.should_not be_detect('hello, world!')
   end
 end


実行してみましょう

$ spec message_filter_spec.rb 
..

Finished in 0.00763 seconds

2 examples, 0 failures
$ 

-fs オプション

さて、ここ以降は RSpec のドキュメント出力機能にも着目します。

spec コマンドの実行時に -fs というオプションをつけると、仕様記述を出力することが出来ます。現状ではどうなっているでしょうか。


実行してみましょう

$ spec -fs message_filter_spec.rb 

MessageFilter
- should detect message with NG word
- should not detect message without NG word

Finished in 0.003184 seconds

2 examples, 0 failures
$ 

RSpec に仕様記述を組み立てさせる

まだ重複しているのは、 it の引数とブロックの中身です。文字列で書かれた内容と、ブロック内のコード自身がかなり重複していますね。 RSpec は it の説明用の文字列引数を省略した時に自分で判断できる範囲で仕様記述を組み立てます。 it の文字列引数を削除してしまいましょう。(日本語の使用についてあとで書く)


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 describe MessageFilter do
   before do
     @filter = MessageFilter.new('foo')
   end
-  it 'should detect message with NG word' do
+  it {
     @filter.should be_detect('hello from foo')
-  end
+  }
-  it 'should not detect message without NG word' do
+  it {
     @filter.should_not be_detect('hello, world!')
-  end
+  }
 end


実行してみましょう

$ spec -fs message_filter_spec.rb 

MessageFilter
- should be detect "hello from foo"
- should not be detect "hello, world!"

Finished in 0.003087 seconds

2 examples, 0 failures
$ 

"should be detect" では英語的に微妙ですが、ここでは目をつぶります。どうでしょう。案外読めるのではないでしょうか?


違和感がある場合、それは設計がまだ不完全であることを示唆していると考えることができますし、カスタムマッチャーへの道を進むこともできます。


ここで論じているようなポイントも RSpec とのうまい付き合いかた、といえます。



describe にコンテクスト情報を追加する

commit f3c46f7030a77ccd6b137d858a50a021775ccbef


先ほどの出力ですが、ちょっと情報が足りないですね

MessageFilter
- should be detect "hello from foo"

「MessageFilter should be detect "hello from foo"」と読めますが、全ての MessageFilter がこう振る舞うわけではないですよね、状況の説明が足りていません。状況説明をテストコードに加えましょう。



message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
-describe MessageFilter do
+describe MessageFilter, 'with argument "foo"' do
   before do
     @filter = MessageFilter.new('foo')
   end
   it {
     @filter.should be_detect('hello from foo')
   }
   it {
     @filter.should_not be_detect('hello, world!')
   }
 end


実行してみましょう

$ spec -fs message_filter_spec.rb 

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

Finished in 0.00303 seconds

2 examples, 0 failures
$ 

ドキュメントを文字列で書かずとも、情報量がかなり保たれるようになってきましたね。これも RSpec Way です。



まだまだ重複がある!

@filter も重複していませんか? これも取り去ることができます。RSpec の subject という機能を使います。subject を使うと、 subject ブロックの評価結果が it 内の should のレシーバになります。


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 describe MessageFilter, 'with argument "foo"' do
   before do
     @filter = MessageFilter.new('foo')
   end
+  subject { @filter }
   it {
-    @filter.should be_detect('hello from foo')
+    should be_detect('hello from foo')
   }
   it {
-    @filter.should_not be_detect('hello, world!')
+    should_not be_detect('hello, world!')
   }
 end

実行してみましょう

$ spec -fs message_filter_spec.rb 

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

Finished in 0.003051 seconds

2 examples, 0 failures
$ 

まだまだまだ重複がある!!

commit 8cdf08ca0052a43780458522248e3746b60c9b7b


今回の例では before ブロックはほとんど仕事していないですね、 subject ブロックの中にインライン化してしまいましょう

message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 describe MessageFilter, 'with argument "foo"' do
-  before do
-    @filter = MessageFilter.new('foo')
-  end
-  subject { @filter }
+  subject { MessageFilter.new('foo') }
   it {
     should be_detect('hello from foo')
   }
   it {
     should_not be_detect('hello, world!')
   }
 end


実行してみましょう

$ spec -fs message_filter_spec.rb 

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

Finished in 0.003051 seconds

2 examples, 0 failures
$ 

簡潔さは力

かなりテストコードが簡潔になってました。コードをすっきりさせたいので it の改行を廃し、最終的にテストコードはこういう形になりました。重複が少なく、かつ情報量自体はかなり保たれています。


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 describe MessageFilter, 'with argument "foo"' do
   subject { MessageFilter.new('foo') }
   it { should be_detect('hello from foo') }
   it { should_not be_detect('hello, world!') }
 end


実行してみましょう

$ spec -fs message_filter_spec.rb 

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

Finished in 0.003051 seconds

2 examples, 0 failures
$ 

第一イテレーション終了!

さて、このイテレーションでは最終的にコードは以下のようになりました。

message_filter.rb

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


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 describe MessageFilter, 'with argument "foo"' do
   subject { MessageFilter.new('foo') }
   it { should be_detect('hello from foo') }
   it { should_not be_detect('hello, world!') }
 end


タグを打ってイテレーションの終了としましょう。

$ git tag -a -m 'end of 1st iteration' end_of_iter1
$ 

この後のログ

未完なのでとりあえず git log を貼っておきます。(長いので別エントリにしたほうがよさそうですね)



(更新) 第2イテレーションも書きました。続きに興味ある方はご覧下さい

commit f004a89c996c962d87a70aed407cd476574bddc5
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 05:13:17 2010 +0900

    add example group for MessageFilter with argument "foo","bar"

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index 23a90ca..bc1c7b7 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -7,3 +7,8 @@ describe MessageFilter, 'with argument "foo"' do
   it { should be_detect('hello from foo') }
   it { should_not be_detect('hello, world!') }
 end
+
+describe MessageFilter, 'with argument "foo","bar"' do
+  subject { MessageFilter.new('foo', 'bar') }
+  it { should be_detect('hello from bar') }
+end
    
    
commit c2ee1bd03d4ad2be03537fe0b71e670db75046e0
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 05:19:36 2010 +0900

    naive implementation using varargs

diff --git a/message_filter.rb b/message_filter.rb
index 1f2c0a1..d032457 100644
--- a/message_filter.rb
+++ b/message_filter.rb
@@ -1,8 +1,11 @@
 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
    
    
commit c33b54b72507989cbf323bfd433aa2e8eb7410b7
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 05:23:25 2010 +0900

    MessageFilter with argument "foo","bar" should satisfy specs for 'foo' only.

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index bc1c7b7..3c1e80c 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -11,4 +11,6 @@ end
 describe MessageFilter, 'with argument "foo","bar"' do
   subject { MessageFilter.new('foo', 'bar') }
   it { should be_detect('hello from bar') }
+  it { should be_detect('hello from foo') }
+  it { should_not be_detect('hello, world!') }
 end
    
    
commit 18c8e8ae5a037c156632b45db5263015ee668a07
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 05:26:34 2010 +0900

    share_examples_for 'MessageFilter with argument "foo"'

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index 3c1e80c..91415d9 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -2,15 +2,18 @@ require 'rubygems'
 require 'spec'
 require 'message_filter'
 
-describe MessageFilter, 'with argument "foo"' do
-  subject { MessageFilter.new('foo') }
+share_examples_for 'MessageFilter with argument "foo"' do
   it { should be_detect('hello from foo') }
   it { should_not be_detect('hello, world!') }
 end
 
+describe MessageFilter, 'with argument "foo"' do
+  subject { MessageFilter.new('foo') }
+  it_should_behave_like 'MessageFilter with argument "foo"'
+end
+
 describe MessageFilter, 'with argument "foo","bar"' do
   subject { MessageFilter.new('foo', 'bar') }
   it { should be_detect('hello from bar') }
-  it { should be_detect('hello from foo') }
-  it { should_not be_detect('hello, world!') }
+  it_should_behave_like 'MessageFilter with argument "foo"'
 end
    
    
commit a50c1a402f1b5dfa4a609efba58324590bb938cd
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 05:32:27 2010 +0900

    nest example groups to make them more DRY

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index 91415d9..fca16fb 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -2,18 +2,18 @@ require 'rubygems'
 require 'spec'
 require 'message_filter'
 
-share_examples_for 'MessageFilter with argument "foo"' do
-  it { should be_detect('hello from foo') }
-  it { should_not be_detect('hello, world!') }
-end
-
-describe MessageFilter, 'with argument "foo"' do
-  subject { MessageFilter.new('foo') }
-  it_should_behave_like 'MessageFilter with argument "foo"'
-end
-
-describe MessageFilter, 'with argument "foo","bar"' do
-  subject { MessageFilter.new('foo', 'bar') }
-  it { should be_detect('hello from bar') }
-  it_should_behave_like 'MessageFilter with argument "foo"'
+describe MessageFilter do
+  share_examples_for 'MessageFilter with argument "foo"' do
+    it { should be_detect('hello from foo') }
+    it { should_not be_detect('hello, world!') }
+  end
+  describe 'with argument "foo"' do
+    subject { MessageFilter.new('foo') }
+    it_should_behave_like 'MessageFilter with argument "foo"'
+  end
+  describe 'with argument "foo","bar"' do
+    subject { MessageFilter.new('foo', 'bar') }
+    it { should be_detect('hello from bar') }
+    it_should_behave_like 'MessageFilter with argument "foo"'
+  end
 end
    
    
commit ad7819f5d9d244d81b830217fcad22be62e3de42
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 05:34:19 2010 +0900

    describe() for things, context() for context

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index fca16fb..244f5c0 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -7,11 +7,11 @@ describe MessageFilter do
     it { should be_detect('hello from foo') }
     it { should_not be_detect('hello, world!') }
   end
-  describe 'with argument "foo"' do
+  context 'with argument "foo"' do
     subject { MessageFilter.new('foo') }
     it_should_behave_like 'MessageFilter with argument "foo"'
   end
-  describe 'with argument "foo","bar"' do
+  context 'with argument "foo","bar"' do
     subject { MessageFilter.new('foo', 'bar') }
     it { should be_detect('hello from bar') }
     it_should_behave_like 'MessageFilter with argument "foo"'
    
    
commit 289edb2b695e8ff0606ce84df882874ae6998361
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 05:36:43 2010 +0900

    now it is time to refactoring product code. use Enumerable#any? for simplicity

diff --git a/message_filter.rb b/message_filter.rb
index d032457..b8492ec 100644
--- a/message_filter.rb
+++ b/message_filter.rb
@@ -3,9 +3,6 @@ class MessageFilter
     @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
    
    
commit 5b810bcb67232df32256e5509b046512f5ee55dd
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 05:47:03 2010 +0900

    new example : 'ng_words should not be empty' in naive style

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index 244f5c0..1b41200 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -10,6 +10,9 @@ describe MessageFilter do
   context 'with argument "foo"' do
     subject { MessageFilter.new('foo') }
     it_should_behave_like 'MessageFilter with argument "foo"'
+    it 'ng_words should not be empty' do
+      subject.ng_words.empty?.should == false
+    end
   end
   context 'with argument "foo","bar"' do
     subject { MessageFilter.new('foo', 'bar') }
    
    
commit 5cb3d978533e8002ab8b6da227ec6521bffcac42
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 05:48:40 2010 +0900

    obvious implementation using attr_reader :ng_words

diff --git a/message_filter.rb b/message_filter.rb
index b8492ec..502eb18 100644
--- a/message_filter.rb
+++ b/message_filter.rb
@@ -1,8 +1,10 @@
 class MessageFilter
   def initialize(*words)
-    @words = words
+    @ng_words = words
   end
+  attr_reader :ng_words
+
   def detect?(text)
-    @words.any?{|w| text.include?(w) }
+    @ng_words.any?{|w| text.include?(w) }
   end
 end
    
    
commit 5974880a2865962de553349e630abc00467edad6
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 05:50:46 2010 +0900

    using be_empty

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index 1b41200..5ec83f8 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -11,7 +11,7 @@ describe MessageFilter do
     subject { MessageFilter.new('foo') }
     it_should_behave_like 'MessageFilter with argument "foo"'
     it 'ng_words should not be empty' do
-      subject.ng_words.empty?.should == false
+      subject.ng_words.should_not be_empty
     end
   end
   context 'with argument "foo","bar"' do
    
    
commit 19ac7de474f422effb49cba0b00ab48b2225cf26
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 05:52:41 2010 +0900

    remove example description argument, but information is wrong

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index 5ec83f8..0c3ccbf 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -10,9 +10,7 @@ describe MessageFilter do
   context 'with argument "foo"' do
     subject { MessageFilter.new('foo') }
     it_should_behave_like 'MessageFilter with argument "foo"'
-    it 'ng_words should not be empty' do
-      subject.ng_words.should_not be_empty
-    end
+    it { subject.ng_words.should_not be_empty }
   end
   context 'with argument "foo","bar"' do
     subject { MessageFilter.new('foo', 'bar') }
    
    
commit 8a971b9e5c5f7eb1dc2e0cb19984ebad459825d4
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 05:54:08 2010 +0900

    using its(:ng_words) to get right amount of information

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index 0c3ccbf..a7dbb92 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -10,7 +10,7 @@ describe MessageFilter do
   context 'with argument "foo"' do
     subject { MessageFilter.new('foo') }
     it_should_behave_like 'MessageFilter with argument "foo"'
-    it { subject.ng_words.should_not be_empty }
+    its(:ng_words) { should_not be_empty }
   end
   context 'with argument "foo","bar"' do
     subject { MessageFilter.new('foo', 'bar') }
    
    
commit 94ee574c81ab8d70882519c3099e0f520df59619
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 05:55:03 2010 +0900

    move to shared examples

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index a7dbb92..cd28e79 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -6,11 +6,11 @@ describe MessageFilter do
   share_examples_for 'MessageFilter with argument "foo"' do
     it { should be_detect('hello from foo') }
     it { should_not be_detect('hello, world!') }
+    its(:ng_words) { should_not be_empty }
   end
   context 'with argument "foo"' do
     subject { MessageFilter.new('foo') }
     it_should_behave_like 'MessageFilter with argument "foo"'
-    its(:ng_words) { should_not be_empty }
   end
   context 'with argument "foo","bar"' do
     subject { MessageFilter.new('foo', 'bar') }
    
    
commit b4a15fa8c971daec40c9c6e0362582bc9e6467c6
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 05:57:53 2010 +0900

    add example: ng_words size is 1

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index cd28e79..ef00f74 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -11,6 +11,9 @@ describe MessageFilter do
   context 'with argument "foo"' do
     subject { MessageFilter.new('foo') }
     it_should_behave_like 'MessageFilter with argument "foo"'
+    it 'ng_words size is 1' do
+      subject.ng_words.size.should == 1
+    end
   end
   context 'with argument "foo","bar"' do
     subject { MessageFilter.new('foo', 'bar') }
    
    
commit 393aaaf73e4f93efeab26648dbffd738faa74645
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 06:01:36 2010 +0900

    introduce have(count).items matcher for size expectation

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index ef00f74..7542f74 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -12,7 +12,7 @@ describe MessageFilter do
     subject { MessageFilter.new('foo') }
     it_should_behave_like 'MessageFilter with argument "foo"'
     it 'ng_words size is 1' do
-      subject.ng_words.size.should == 1
+      subject.ng_words.should have(1).items
     end
   end
   context 'with argument "foo","bar"' do
    
    
commit 77ff82504d0e490525190b08a9b1868bb6cee77e
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 06:02:45 2010 +0900

    remove example description, but information is wrong

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index 7542f74..1f7593d 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -11,9 +11,9 @@ describe MessageFilter do
   context 'with argument "foo"' do
     subject { MessageFilter.new('foo') }
     it_should_behave_like 'MessageFilter with argument "foo"'
-    it 'ng_words size is 1' do
+    it {
       subject.ng_words.should have(1).items
-    end
+    }
   end
   context 'with argument "foo","bar"' do
     subject { MessageFilter.new('foo', 'bar') }
    
    
commit 3d0226cae2093deb27f1211cf547e689042d96ba
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 06:03:51 2010 +0900

    okay, introduce its(:ng_words) again.

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index 1f7593d..3d54706 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -11,9 +11,7 @@ describe MessageFilter do
   context 'with argument "foo"' do
     subject { MessageFilter.new('foo') }
     it_should_behave_like 'MessageFilter with argument "foo"'
-    it {
-      subject.ng_words.should have(1).items
-    }
+    its(:ng_words) { should have(1).items }
   end
   context 'with argument "foo","bar"' do
     subject { MessageFilter.new('foo', 'bar') }
    
    
commit 8b0ca415de0bfd6af512d304fb475335cec6cae3
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 06:06:24 2010 +0900

    using have(n).named_collection to gain simplicity

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index 3d54706..85f106a 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -11,7 +11,7 @@ describe MessageFilter do
   context 'with argument "foo"' do
     subject { MessageFilter.new('foo') }
     it_should_behave_like 'MessageFilter with argument "foo"'
-    its(:ng_words) { should have(1).items }
+    it { should have(1).ng_words }
   end
   context 'with argument "foo","bar"' do
     subject { MessageFilter.new('foo', 'bar') }
    
    
commit a75ba296ca465eb21a23661aa6c8c0ae0f3794a1
Author: Takuto Wada <takuto.wada@gmail.com>
Date:   Sun Feb 28 06:07:23 2010 +0900

    add similar example for other context

diff --git a/message_filter_spec.rb b/message_filter_spec.rb
index 85f106a..e738ea8 100644
--- a/message_filter_spec.rb
+++ b/message_filter_spec.rb
@@ -17,5 +17,6 @@ describe MessageFilter do
     subject { MessageFilter.new('foo', 'bar') }
     it { should be_detect('hello from bar') }
     it_should_behave_like 'MessageFilter with argument "foo"'
+    it { should have(2).ng_words }
   end
 end