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


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


大きく時間が開いてしまいました(すみません…)、RSpec 入門の第三イテレーションです。
(第3回 coffee.rb の開催に合わせたライブ更新で書かれましたので、まだ詳細の説明は途中のところもあります。)

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

この「RSpec 入門とその一歩先へ」シリーズでは、メッセージフィルタを RSpec を使って開発することで、 RSpec の機能と TDD を同時に学ぶことを狙いとしています。


前回終了時点のコードと実行結果をまず記します。


message_filter.rb

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

message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 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
   context 'with argument "foo"' do
     subject { MessageFilter.new('foo') }
     it_should_behave_like 'MessageFilter with argument "foo"'
   end
   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"'
   end
 end


(-fs オプション付きで)実行してみましょう

$ spec -fs 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.013741 seconds

5 examples, 0 failures
$ 

新仕様追加

さて、第3イテレーションでは機能をまた新しく加えてゆきましょう。


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

$ git checkout master
$ git merge 2nd
$ git checkout -b 3rd

第3イテレーション開始

commit 5b810bcb67232df32256e5509b046512f5ee55dd


さて、第3イテレーションでは NG ワードに関する機能を追加します。具体的には、 MessageFilter に NG ワードがいくつ設定されているか、どんな NG ワードが設定されているかが分かるような機能を追加してみます。


最初に書くのは…そう、 spec からですね。第二イテレーションまでの spec の context で既に NG ワードが設定されているはずなので、 NG ワードが空でない、というテストを書いてみましょう。

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 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
   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') }
     it { should be_detect('hello from bar') }
     it_should_behave_like 'MessageFilter with argument "foo"'
   end
 end
subject を明示的なレシーバとして使う

ここで

subject.ng_words.empty?.should == false

という書き方をしています。この subject は何かというと、 subject ブロックの評価結果が返るメソッドです。

前回までのイテレーションでは、 it ブロックの中で should のレシーバが暗黙的に subject ブロックの評価結果になるという説明をしてきましたが、明示的に subject ブロックの評価結果をテストコード内で使いたい場合には、 subject メソッドを使うことが出来ます(ちょっと紛らわしいですね)。



では実行してみましょう

$ spec message_filter_spec.rb 
..F...

1)
NoMethodError in 'MessageFilter with argument "foo" ng_words should not be empty'
undefined method `ng_words' for #<MessageFilter:0xb7886d48 @words=["foo"]>
./message_filter_spec.rb:14:

Finished in 0.02881 seconds

6 examples, 1 failure
$ 

ng_words というメソッドが無いというエラーが発生して、めでたく(?)、失敗しました。まだ実装していない機能に対するテストですから、落ちるのが当然ですね。 TDD では大事なステップです。では実装を行いましょう。


明白な実装

commit 5cb3d978533e8002ab8b6da227ec6521bffcac42


前のステップで追加した spec は、コードの利用者から見た視点で書きました。このテストを通すためにまた「仮実装」をしても良いのですが、ここは一気に実装まで行う「明白な実装(Obvious Implementation)」路線で行ってみます。 attr_reader で実装できそうですね。


message_filter.rb

 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

@words というインスタンス変数を @ng_words と書き換え、次に attr_reader を定義して外からメソッドとして見えるようにしました。ここにも、テストから先に書く意味と効果が現れています。

ここまでコードを書いてきた実装者の視点では、 MessageFilter の内部変数として @words という名前を使うことは簡潔で良いかもしれません。しかし、テストを先に書くという視点、つまりコードの利用者から見た視点ではどうでしょうか。 message_filter.words というメソッドでは、「 words 」が NG ワードを指すのか、それとも検出された言葉を指すのかが分かりにくくなてしまっていないでしょうか。

最初の利用者は自分

TDD では、実装の前にテストを書きます。これは、半ば強制的に「利用者の視点」を得ることが出来る、という効果を持ちます。まだ実装されていない機能のテストなのですから「自分はこういうメソッドがわかりやすい」「こういう名前が良い」、逆に「こういう名前は曖昧で不安」「引数が文字列5つとかありえない」などのコードを利用する側としての視点や感情を得られます。

TDD では、その感情や利用者としての設計判断をテストコードという形で記し、その後で実装を行うことで、既存の実装に引きずられにくい「こうしたい」「こうあるべき」という実装を引き出しやすくなるという効果があります。

「自分が書くコードの最初の利用者は自分」というのが TDD のルールです。英語では「 Eat your own dog food (自分のドッグフードを食べろ)」というフレーズで知られている考え方でもあります。



では実行してみましょう

$ spec message_filter_spec.rb 
......

Finished in 0.014004 seconds

6 examples, 0 failures
$ 

predicate マッチャに書き換える

commit 5974880a2865962de553349e630abc00467edad6

以前も書きましたが、 真偽値を返すメソッドは predicate マッチャという形式に書き換えることで可読性を上げることが出来ます。

subject.ng_words.empty?.should == false

という書き方は

subject.ng_words.should_not be_empty

という書き方に変更することができます。この方が読みやすく、違和感が無いのではないでしょうか。(subject から始まるところに違和感がありますが…)


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 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
   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
+      subject.ng_words.should_not be_empty
     end
   end
   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"'
   end
 end

(-fs オプション付きで)実行してみましょう

$ spec -fs message_filter_spec.rb 

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

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.01407 seconds

6 examples, 0 failures
$ 

問題なさそうですね。


RSpec に仕様記述をさせる


commit 19ac7de474f422effb49cba0b00ab48b2225cf26


さて、この「RSpec 入門とその一歩先へ」シリーズでは一貫して「なるべく it メソッドの文字列引数を使わずに、 RSpec 自身に仕様記述を組み立てさせる」という方針で進めてきました。

     it 'ng_words should not be empty' do

という記述もいかにも RSpec 自身に組み立てさせることが出来そうです。やってみましょう。


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 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
   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') }
     it { should be_detect('hello from bar') }
     it_should_behave_like 'MessageFilter with argument "foo"'
   end
 end


(-fs オプション付きで)実行してみましょう

$ spec -fs message_filter_spec.rb 

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

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.014026 seconds

6 examples, 0 failures
$ 

む? なにかおかしいですね…


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

MessageFilter with argument "foo"
(中略)
- should not be empty

「 MessageFilter with argument "foo" should not be empty 」…意味を正確に伝えなくなってしまいました。 RSpec にもっとヒントを与えなくてはならないようですね。



its メソッド

commit 8a971b9e5c5f7eb1dc2e0cb19984ebad459825d4


RSpec に情報を与えるために、ここで its というメソッドを使ってみます。


its メソッドは引数に Symbol を受け取ります。そして subject に対して Symbol で指定されたメソッドを呼び出し、その戻り値を its のブロック内での should の暗黙のレシーバに設定します。


ちょっと複雑なので、実際に例を見た方が良いかもしれません。今回のテストコードは、 its 機能を使って次のように書き換えることが出来ます。


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 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
   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') }
     it { should be_detect('hello from bar') }
     it_should_behave_like 'MessageFilter with argument "foo"'
   end
 end


(-fs オプション付きで)実行してみます。さて、どうなるでしょう。

$ spec -fs message_filter_spec.rb 

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

MessageFilter with argument "foo" ng_words
- should not be empty

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.016423 seconds

6 examples, 0 failures
$ 

出力が変わりましたね。

MessageFilter with argument "foo" ng_words
- should not be empty

「 MessageFilter with argument "foo" ng_words should not be empty 」…ぎこちなくはありますが、補足情報が加わりました。


それよりも、大きく可読性が上がったのはテストコードの方です。

  its(:ng_words) { should_not be_empty }


括弧や記号を"見えないもの"とすると、「 its ng words should not be empty 」となります。前よりも自然に読めるコードになっているのではないでしょうか。

(TODO: describe/context で同じことをするためには。ネスト、行数。)
(TODO: its の落とし穴とトレードオフについて説明を行う)



share_examples_for に移動する


commit 94ee574c81ab8d70882519c3099e0f520df59619


ところで、「its ng_words should_not be_empty」という仕様は "foo" の場合だけでなく "foo, bar" でも当てはまりますね。なので、 share_examples_for のブロックに移しましょう。


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 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') }
     it { should be_detect('hello from bar') }
     it_should_behave_like 'MessageFilter with argument "foo"'
   end
 end


(-fs オプション付きで)実行してみます。

$ spec -fs message_filter_spec.rb 

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

MessageFilter with argument "foo" ng_words
- should not be empty

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

MessageFilter with argument "foo","bar" ng_words
- should not be empty

Finished in 0.029876 seconds

7 examples, 0 failures
$ 

大丈夫そうですね。



do...end から {...} への変更とその意図について

このシリーズでは、これまでもテストコードの中でブロックの書き方を do...end から {...} への変更を何度か行ってきました。
二つの書き方のどちらを選択するかはプログラマの自由ですが、慣例的に以下のようなルールに従うことが多いと思います。

ただし、メソッドチェインを行う場合は{ ... }を使用する

Rubyコーディング規約

まず基本的な使いわけとして、複数回呼ぶ可能性があるとき (例えば Enumerable#each) は do....end を使う。呼ぶのが一回だけのとき (例えば File.open) は {....} を使う。ここで、後者の「一回だけのとき」とは RubyIteratorPattern で言う「範囲型」あるいは「コンテキスト型」のことである。

加えて、一行に収めるときは {....} を使う。

さらに、返り値を使うときも {....} を使う。

http://i.loveruby.net/w/RubyCodingStyle.html


RSpec の場合は、it のブロックの戻り値を使うことは(多分)ありません。つまりメソッドチェインを使いたいという動機はありません。

なので、 RSpec を使ったコードの中で do...end を使うか {...} を使うかは、純粋に可読性のための選択ということになります。例えば今回の場合は、「一行に収まるので {...} を選択する」というルールに従ったとも言えます。


しかし私は、 RSpec のコードの中で it {...} というスタイルを選択することに、もっと積極的な意味を持たせています。それは「 RSpec 自身に状況を理解させ、テストコードの可読性と仕様記述を両立させる」ということです。


先ほど説明の中で「括弧や記号を"見えないもの"とすると」というフレーズを使いました。 RSpec を使ったコードの中では、視覚的に強くない it{...} 形式は黒子のような役割を果たし、テストコード自身の可読性に寄与すると考えています。つまり、文字列によってではなく、テストコード自身に仕様を語らせるときに it {...} を使うようにしています。


テストコードをリファクタリングするときは、 RSpecプログラマの間で情報を共有しつつ、記述量も少なく簡潔で、かつ大きい情報の欠落が無い、そんなコードを目指しています。


(TODO: テストコードの可読性をとるか、 -fs 形式の可読性をとるか。それは排他的なのか)




もう一つ機能追加: NG ワードの個数を調べる

commit b4a15fa8c971daec40c9c6e0362582bc9e6467c6


さて、もう一つ機能追加してみましょう。「NG ワードの個数を調べる」という機能のテストと実装を行います。最初に書くのはもちろんテストです。

message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 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"'
+    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') }
     it { should be_detect('hello from bar') }
     it_should_behave_like 'MessageFilter with argument "foo"'
   end
 end


(-fs オプション付きで)実行してみます。

$ spec -fs message_filter_spec.rb 

MessageFilter with argument "foo"
- should be detect "hello from foo"
- should not be detect "hello, world!"
- ng_words size is 1

MessageFilter with argument "foo" ng_words
- should not be empty

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

MessageFilter with argument "foo","bar" ng_words
- should not be empty

Finished in 0.028826 seconds

8 examples, 0 failures
$

おや、実装していないのに一発で通りましたね。つまり attr_reader を導入していたので実装は必要なかった、ということです。


have マッチャ

commit 393aaaf73e4f93efeab26648dbffd738faa74645

  subject.ng_words.size.should == 1

という書き方は明示的ではありますが、ややぎこちないですね。このような場合、 RSpec では「 have マッチャ」という機能で書き方を改善することができます。

具体的には、次のように書けます

  subject.ng_words.should have(1).items

(TODO: items について補足する)


では have マッチャを導入してみましょう。


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 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"'
     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
     subject { MessageFilter.new('foo', 'bar') }
     it { should be_detect('hello from bar') }
     it_should_behave_like 'MessageFilter with argument "foo"'
   end
 end


実行してみます。

$ spec -fs message_filter_spec.rb 

MessageFilter with argument "foo"
- should be detect "hello from foo"
- should not be detect "hello, world!"
- ng_words size is 1

MessageFilter with argument "foo" ng_words
- should not be empty

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

MessageFilter with argument "foo","bar" ng_words
- should not be empty

Finished in 0.028826 seconds

8 examples, 0 failures
$

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

commit 77ff82504d0e490525190b08a9b1868bb6cee77e


再度、テストコード記述の合理化を進めましょう。it の文字列引数を廃し、 RSpec に仕様記述を組み立てさせます。


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 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"'
-    it 'ng_words size is 1' do
-      subject.ng_words.should have(1).items
-    end
+    it { subject.ng_words.should have(1).items }
   end
   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"'
   end
 end


(-fs オプション付きで)実行してみます。

$ spec -fs message_filter_spec.rb 

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

MessageFilter with argument "foo" ng_words
- should not be empty

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

MessageFilter with argument "foo","bar" ng_words
- should not be empty

Finished in 0.028714 seconds

8 examples, 0 failures
$

うむむ、またも情報不足ですね。

MessageFilter with argument "foo"
(中略)
- should have 1 items

と、情報量が減ってしまっています。さてどうしましょう。


覚えたてホヤホヤの its を使ってみる

commit 3d0226cae2093deb27f1211cf547e689042d96ba


先程せっかく覚えたのですから、覚えたてホヤホヤの its を使ってみましょう


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 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"'
-    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') }
     it { should be_detect('hello from bar') }
     it_should_behave_like 'MessageFilter with argument "foo"'
   end
 end


(-fs オプション付きで)実行してみます。

$ spec -fs message_filter_spec.rb 

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

MessageFilter with argument "foo" ng_words
- should not be empty

MessageFilter with argument "foo" ng_words
- should have 1 items

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

MessageFilter with argument "foo","bar" ng_words
- should not be empty

Finished in 0.035714 seconds

8 examples, 0 failures
$ 

情報が付加されましたね。これで満足…でしょうか?



have(n).named_collection 記法を使う

commit 8b0ca415de0bfd6af512d304fb475335cec6cae3


実はもっと良い書き方があります。これまで have マッチャに対して 'items' というメソッドを呼んでいましたが、have マッチャはレシーバ(subject)がコレクションを返すメソッドを持つ場合に、そのメソッド名を記述することができます。

 subject.should have(n).named_collection 

という書き方です。ここで named_collection に subject が持つメソッド名を使うことが出来るのです。 RSpec は指定されたメソッドを subject に対して呼び出し、結果の数を元に検証を行います。


RailsActiveRecord のモデルを想像するとわかりやすいかもしれません。例えば

class Blog < ActiveRecord::Base
  has_many :comments
end

というモデルがある場合、

 some_blog.should have(4).comments 

と書くことができるということです。



では今回の例ではどうなるでしょうか。書いてみましょう。

message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 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 have(1).items }
+    it { should have(1).ng_words }
   end
   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"'
   end
 end


(-fs オプション付きで)実行してみます。

$ spec -fs message_filter_spec.rb 

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

MessageFilter with argument "foo" ng_words
- should not be empty

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

MessageFilter with argument "foo","bar" ng_words
- should not be empty

Finished in 0.028904 seconds

8 examples, 0 failures
$ 

どうでしょう。

MessageFilter with argument "foo"
(中略)
- should have 1 ng_words

出力は前よりも良い具合になりましたね。


テストコードの方も

    its(:ng_words) { should have(1).items }

から

    it { should have(1).ng_words }

へと書き換わりました。どうでしょう。読みやすくなっているのではないでしょうか。

(TODO: its の内部動作、クラスの生成について)


仕上げ

commit a75ba296ca465eb21a23661aa6c8c0ae0f3794a1

もう一つの context の方も have(n).named_collection 記法でテストを書いて、このイテレーションの締めとしましょう。


message_filter_spec.rb

 require 'rubygems'
 require 'spec'
 require 'message_filter'
 
 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"'
     it { should have(1).ng_words }
   end
   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"'
+    it { should have(2).ng_words }
   end
 end


(-fs オプション付きで)実行してみます。

$ spec -fs message_filter_spec.rb 

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

MessageFilter with argument "foo" ng_words
- should not be empty

MessageFilter with argument "foo","bar"
- should be detect "hello from bar"
- should be detect "hello from foo"
- should not be detect "hello, world!"
- should have 2 ng_words

MessageFilter with argument "foo","bar" ng_words
- should not be empty

Finished in 0.028731 seconds

9 examples, 0 failures
$ 

テストが全て通りました。まずは一安心です。

(TODO: 「 have(1).ng_word 」とは書けないの?)



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

このイテレーションで学んだことをまとめてみます。

  • subject を明示的なレシーバとして使うこともできる
  • 自分が最初のユーザ
  • its メソッド
  • have マッチャ
  • have(n).named_collection 記法
  • RSpec にヒントを与え、 RSpec 自身に仕様を記述させることで、テストコード自体も自然と短い記述に近づくという、 RSpec のコツ


(第4イテレーションへ続く?)