RSpecよりShoulda、fixturesよりヘルパーとMocha
RailsでBDDと言ったらRSpecが圧倒的にポピュラーですね。でもRSpecに馴染めないという人はいませんか?私はx.should == yという書き方からKernelを拡張する実装まで、何もかも独自路線でいく個性の強さについていけません。
しかし同時にTest::Unitの垢抜けなさにもうんざりしていて、何かいいフレームワークはないかと思っていた時にShouldaに出会いました。contextを入れ子にできる柔軟性や、既存のTest::Unitインスタンスの中に書けるとっつきやすさはとても魅力的に見えました。そしてその日からShouldaでテストを書き始めました。
このエントリでは、Shouldaを中心にヘルパーやMochaを使った気持ちのいい開発手法を紹介します。
Shouldaは柔軟でとっつきやすい
RSpecの最大の魅力はその書き方にあると思います。しかしその書き方を利用するために、RSpecが開発者に多くのルールを課すのも事実です。いくつか挙げてみると、まず当然ながらそのシンタックスそのもの。ファイル構成(*_spec.rb)とコマンド(spec)、難解なRSpec自身の実装、独自の実行結果のフォーマット(x examples, y failures)。これらはRSpec導入の敷居を高くし、他のツールとの親和性を損なっていると思われます。
ShouldaはBDDのための最低限の文法を提供し、その他には何も変更しません。下のコードを見てください。
class UserTest < Test::Unit::TestCase def setup # Normal setup code end def test_can_have_normal_tests # Normal test here end context "a User instance" do setup do @user = User.find(:first) end should "return its full name" do assert_equal 'John Doe', @user.full_name end end end
(:: GIANT ROBOTS SMASHING INTO OTHER GIANT ROBOTS :: Introducing the Shoulda Testing Pluginより。)
前半は古きよき?Test::Unitそのものだということが分かるでしょう。そしてこのコードは「test: a User instance should return its full name」という名前のテストを生成し、「rake」で実行でき、「2 tests, 2 assertions, 0 failures, 0 errors」という、標準的な結果表示をしてくれます。
さらに、Shouldaのcontextは入れ子にできます。
class UserTest << Test::Unit context "A User instance" do setup do @user = User.find(:first) end should "return its full name" do assert_equal 'John Doe', @user.full_name end context "with a profile" do setup do @user.profile = Profile.find(:first) end should "return true when sent #has_profile?" do assert @user.has_profile? end end end end
(thoughtbot: Shoulda testing pluginより)
このフィーチャーは自然な形でコードをDRY UPしてくれます。
RSpecが様々なspecを定義するのに便利なシンタックスを提供してくれるように、Shouldaにも独自のマクロがあります。またShouldaはその簡潔な実装ゆえに新しいマクロを実装しやすくなっています。詳しくはShouldaのサイトを見てください。
fixturesは避ける。しかしモックにも欠点が
フィクスチャが欠点だらけということは広く認識されていると思いますが、改めて書くと、
- 書きにくい
- 分かりにくい(特にassociation)
- 遅い
同じテスト用データをマクロ1発でロードできるのでDRYではありますが、とにかく素のRailsで(プラグインの助けを借りずに)フィクスチャでテストしようと思ったら茨の道であることは間違いありません。
そこで多くの人はモックとスタブを使いますが、モック/スタブにも次のような欠点があると思います。
製品コードの柔軟性が失われやすい。
つまり製品コードが使用するインターフェースをモック/スタブが決めてしまい、他の書き方をできなくなってしまうという現象です。
たとえばPeopleモデルのレコード数を数える場合、ただ数えるだけならPeople.countを使いますね。そこでPeople.countをモックするわけですが、数えるだけでなく実際に全てのレコードを使うなら、People.find(:all)して得られたArrayのsizeを使えばSQLの発行は1回で済みます。ではPeople.findもモックするのか?実際の製品コードではどちらでも書けるようにしておけば、リファクタリングのための自由度を残せます。つまりモック/スタブを使わず、DBにアクセスさせる方が開発しやすい場合があるということです。
ヘルパーでセットアップ
ではフィクスチャの欠点を避けつつ、モック/スタブの罠にもかからないようにDBを使うにはどうすればいいでしょうか。要件は次の通りです。
- 書きやすい
- 読みやすい
- fixturesより速い
- DRYである
- 製品コードに自由度を残す
答えはテスト用データをセットアップするためのヘルパー群です。製品コードに自由度を残すためにDBは使いますがfixturesは使わないので、セットアップの段階で必要なデータをDBに注入してやる必要があります。しかし毎回People.create :name => ...とやっていたのではDRYでなくなってしまうので、ヘルパーを作るのです。例えば次のようなコードです。
def a_person People.create! :name => random_name, :email => random_email end
なおrandom_nameとrandom_emailは、文字通りランダムな名前とメールアドレスを生成するヘルパーです。ランダムでいいのか疑問を持たれる方もいらっしゃると思いますが、assertionの段階で参照さえできれば、ほとんどのテストはランダムな値で十分です。どうしても特定の値を持たせたければヘルパーを使わずにPeople.createを直接呼べばよいだけです。ヘルパーをdef a_person(name, email)などと定義して柔軟性を持たせるよりも、何度も書くヘルパーを短くする方が重要です。
このようなヘルパーを使ってどのようなテストを書けるか、以下のまとめで紹介します。
まとめ
それではShouldaとヘルパーを使ったコードの実例です。setupの中のヘルパーで汎用的なテスト用データを生成し、それらをテストの中で使用しています。
class CommentObserverTest < Test::Unit::TestCase context 'With an issue, adding a comment' do setup do @issue, @client = an_issue_of_a_client emails.clear end should 'send a notification' do a_comment(@issue, an_account(@client), :private) assert_equal 1, emails.size end should 'not notify 3rd party members a private comment' do a_comment(@issue, an_account(a_client), :private) assert_equal 0, emails.size end end end
このようにShouldaと独自のヘルパーを使うことで、書きやすく、読みやすく、とっつきやすいテストを書くことができました。
最後にShouldaの欠点とMochaについて言及しておきます。Shouldaは個々のテストを「should ...」で書き始めるため、主語をcontextの方に書く必要があります。ところがcontextにつけるフレーズは主語として適切でないことがしばしばあるのです(上の例のように)。そのためcontextのフレーズと実際のsetupの中に書かれている内容がずれてしまうことがあるのが悩みです。
Mochaはモックとスタブを提供するプラグインなのですが、RSpecが独自にモックとスタブを提供するのに対しShouldaはそうでないので、モックやスタブが必要な時は適当なプラグインだと思います。