Shoulda で Rails のテストにコンテキストを導入する

Ruby on Rails (以下,Rails)標準のテスティングフレームワークである Test::Unit は,一般にはフラットにテストメソッドを定義するものだと思われています(たとえば,Rubyist Magazine - 改めて学ぶ RSpec).しかし,Test::Unit にコンテキストを導入して階層化するのは意外と簡単です.このエントリでは,Test::Unit にコンテキストを導入する方法として,ActiveSupport::TestCase にモンキーパッチを当てる方法と Shoulda を導入する方法を紹介します.さらに,一つ前のエントリで紹介した ActiveRecord モデルテストの例を Shoulda のコンテキストを使って書き直し,コードがコンパクトになることを示します.

RSpec の context に憧れて

前エントリの Rails の ActiveRecord モデルテストのガイドラインActiveRecord モデルテストを書いたときには,テストメソッドはフラットに並んでいました.参考までに gist を貼りつけておきます.

このテストケースを見ると,「実は ActiveRecord モデルの状態毎にテストメソッドをまとめられるのでは?」とか,「RSpec ならできるのに」とか考える人がいるかもしれません.いやいや,Test::Unit でも簡単に context を導入できるんですよ.まずは,原始的な方法から説明します.

ActiveSupport::TestCase にモンキーパッチ

まずは,こちらをご覧ください.

# test/test_helper.rb
class ActiveSupport::TestCase
def self.context(description = nil)
yield
end
end

何のことはない,ActiveSupport::TestCase のクラスメソッドとして context() を定義しただけです.context() が何をするかというと,何もしません.渡されたブロックを,ただ yield するだけです.しかし,これだけで,RSpec 風の context を Test::Unit で使えるようになります.最初に挙げたフラットなテストケースを context() を使って書き直したものを gist で貼り付けます↓

これは便利なので gem にして公開しようかとも思ったのですが,その必要はありませんでした.もっと便利な Shoulda を使いましょう.

Shoulda

Shoulda を使うと Rails のテストで RSpec のような context を定義できるようになります.Shoulda は gem として提供されているのでインストールは簡単です.Gemfile に

gem 'shoulda', groups: [:test]

として,bundle install すれば完了です.Shoulda を使ったテストの場合,コンテキスト内では test() の代わりに context() を使うことになっているので,モンキーパッチ版テストケースの test ブロックを should ブロックに置き換えます.置き換えたもの例によって gist で貼ります↓

Shoulda の context()/should() を使うと rake test の出力結果も変わります↓

ItemTest
test: with blank title should an item with blank title is invalid. PASS
test: with blank title should db can catch blank title. PASS
test: with boundary value attributes should an item with boundary value attributes is valid. PASS
test: with boundary value attributes should db allows saving an item with boundary value attributes. PASS
test: with empty price should an item with empty price is invalid. PASS
test: with empty price should db can catch null price. PASS
test: with empty title should an item with empty title is invalid. PASS
test: with empty title should db can catch null title. PASS
test: with negative price should an item with negative price is invalid. PASS
test: with negative price should db can catch negative price. PASS
test_db_can_catch_an_item_with_invalid_reference PASS
test_db_can_catch_null_vendor_id PASS

各テストメソッドが,test_ ではじまるテストメソッド名ではなく,"{context} should {should}" という文になっていることがわかります.この結果では,test_ テストメソッドのときの名前のまま放置していたので,おかしな文になっています.もう少し英語らしくなるように名前を修正してみます.gist ↓

これを rake test すると,

ItemTest
test: an item with blank title should an item with blank title is invalid. PASS
test: an item with blank title should be rejected by db. PASS
test: an item with boundary value attributes should be accepted by db. PASS
test: an item with boundary value attributes should be valid. PASS
test: an item with empty price should be invalid. PASS
test: an item with empty price should be rejected by db. PASS
test: an item with empty title should be invalid. PASS
test: an item with empty title should be rejected by db. PASS
test: an item with negative price should be invalid. PASS
test: an item with negative price should be rejected by db. PASS
test_db_can_catch_an_item_with_invalid_reference PASS
test_db_can_catch_null_vendor_id PASS

と少しはましになりました.

context 毎の setup

Rails の Test::Unit では,テストケース毎に 1 つの setup() をもち,この setup() は各テストメソッドで共通でした.Shoulda を使うと,context 毎に setup() を設定することができるようになります.前のテストケースでは,同一コンテキスト内で同じファクトリを何度も呼びだしているところがありました.これらはコンテキスト毎にsetup() でまとめられそうです.早速書き直してみたものを,またまたまた gist で貼りつけておきます↓

あれっ,かえって冗長になってしまいました.Shoulda のコンテキスト毎の setup() は役に立たないのでしょうか.実はこれ,間違ってはいないんですが私の考える理想的な setup() の使い方ではないんです.

context() + setup() の本当の実力

前の例ではコンテキスト毎に setup() を定義したためにかえってテストケースが長くなってしまいましたが,よくよく見てみると,ネガティブテストは同じパターンの繰り返しです.こんなとき,善良なる Rails 開発者なら,きっとこの gist のようにします.

これで,テストが短かく dry になりました.

おわりに

このエントリでは,Shoulda で Rails テストにコンテキストを導入する方法を紹介しました.前のエントリで紹介したフラットな Test::Unit テストケースを Shoulda で書き直し,context() とコンテキスト毎の setup() を導入することで,元のコードをコンパクトにすることがきました.実は,コンパクトにするだけなら Shoulda を使わなくても Test::Unit テストケースのままでもできるのですが,Test::Unit のときにはその気が起こらず,Shoulda のときには起きるので不思議です(Shoulda 使用者の主観で Shoulda の効用を裏づけるものではありません).

RSpec を使ってみたけど好きになれなかったという人は Shoulda を試してみてはいかがでしょうか.RSpec にあって Test::Unit には無い context() を補い,あなたのテストコードを小さく理解可能な状態に保つのに役立つはずです.