前回は具体的なWebアプリを例にして簡単なコードレビューをしました。今回からは、テストを使ったリファクタリングについて解説していきます
少し時間が空いてしまいましたが、前回は具体的なWebアプリを例にして簡単なコードレビューをしました。今回からは、そのWebアプリに対してテストを書いてリファクタリングする具体的な方法について解説していきます。
今回はまず、Ruby on Railsで人気のあるテストフレームワークの数々についてご紹介します。
Hamlの作者として知られるHampton Catlin氏が行った「Hampton's Third Ruby Survey, 2010」の中に、テストに関するいくつかの興味深い結果があります。好きなテストフレームワークは何ですかという質問に対する答えをグラフにすると以下の通りです。
これを見ると「ビヘイビア駆動開発(BDD)」のテストフレームワークであるRSpecがダントツの人気を誇っているようです。
これらのテストをカテゴリ別にご紹介していきましょう。
ユニットテストとは、各クラスのメソッド単位で行うテストのことです。各クラスのメソッドの担当する役割の範囲内で、さまざまなケースを想定したテストを書きます。これらを担当するのがTest::Unit、RSpec、Shoulda、Riotなどです。
では、各テストのサンプルを示しつつ、それぞれの特徴を説明していきましょう。
まずは、Rubyに標準で添付されるテストライブラリのTest::Unitです。テストコードのサンプルは以下の通りです。
class UserTest < Test::Unit::TestCase def setup @user = User.new(:email => "foo") end def test_email_address_is_nil assert_equal("foo", @user.email) end end
テストは全てTest::Unit::TestCaseを継承したクラスの中で行われ、実際のテスト文は「test_」の接頭辞で始める必要があります。
「assert_equal(予想値, 実際値)」という形で書くのが普通ですが、「assert_nil」「assert_raise」といった、特定のテストに特化したテストヘルパーメソッドもあります。
このテストフレームワークは、Ruby本体やRailsのフレームワークのテストに使われており、今なお健在です。しかしながら前回にも述べた「テストは仕様書」という考え方を押し進め、なるべく自然言語(英語)に近い形でテストを書くことを目的として作られたテストフレームワークが登場しています。
なるべく自然言語に近い形でテストが書けることで人気が高いのが、RSpecです。RSpecを使うと、以下のようにテストコードが書けます。
describe User do before(:each) do @user = User.new(:email => "foo") end it "should have nil email" do @user.email.should eq "foo" end end
RSpecの一番の特徴として、「@user.email.should eq foo」が「user's email should equal to foo」と自然言語のように読めることでしょう。また、テストに必要な変数の設定などを行うsetupと同等の機能として、beforeというのがあります。beforeの使い方には、全テストの前に一度だけ走らせる方法「before(:all)」と、各テストの前に走らせる「before(:each)」の2つがあります。
私がTest::UnitからRspecに乗り換えたのは2008年頃だったと思いますが、その際に一番便利だと思ったのは、describeを複数指定することが可能なことでした。これによって似た機能のテストをグループごとにくくることができ、テストの見通しが良くなりました。
これと似たことをTest::Unitでしようとすると1つのtest_メソッドに複数のアサーションを書くことになります。こうした場合、最初のアサーションがフェイルすると、残りが実行されないので、結局どれだけのテストがフェイルしているのか分かりづらかったりします。
「テストは仕様書」と言う観点から、RSpecは非常にテストを英語っぽく書くためのいくつかのメソッドをメタプログラミングを通じて提供しています。
例えば、@user.should be_valid というのは @user.valid?.should == true と同義ですが、より英語として読みやすくするためのシンタックスシュガーです。“be_”の後にメソッド名を付けると、テストをする対象のオブジェクトのメソッド名+?を付けたもの(今回の例では“valid?”)がtrueを返すかを調べてくれます。通常、Railsでboolean(true/false)を返すメソッドには形容詞(good/old/valid)を付けるのが一般的なので自然な英語に見えますが、ではArrayの中に特定の値があるかどうか調べる「include?」の場合はどうなるでしょうか?
[1,2,3,4,5].should be_include 3
“be include”というのは英語の文法としておかしいですよね。そこはRSpecチームも心得ているもので、それ専用のさらなるシンタックスシュガーを用意してくれています。
[1,2,3,4,5].should include 3
「これで完璧」と思われるかも知れませんが、何か問題点に気がつきませんか? そう“include”というのはRubyでModuleをミックスインするためのれっきとしたメソッドなのですが、このメソッドを上書きしてしまっているのですね。
実際にitのブロック内でモジュールをincludeすることはまずないとは思いますが、万が一した場合は、予想と反した動きをしてしまうことになります。では「これを解決するためにさらなるシンタックスシュガーを」となると、本来読みやすさを追求するために仕組みが、逆に混乱を招いてしまったり、テストを書きづらくしてしまうことにもなりかねないので、やり過ぎはどうかなという気もします。
最後に、社内にいるRSpec嫌いなイギリス人の同僚の言葉を借りると、「そもそも英語は不規則なルールがてんこ盛りなのに、そんなのに沿うようにコードなんて書いてられるか」だそうです。でもそんな彼は以前カンファレンスでRubyのパパであるまつもとゆきひろさんと同席した際、「なんでelsifはelseifじゃないんだ。eが抜けてるぞ!」と食ってかかっていましたが……。
Shouldaは、既存のTest::UnitをRSpecっぽく書けるようにするものです。テストコードのサンプルは以下の通りです。
class UserTest < Test::Unit::TestCase context "New User" do def setup @user = User.new(:email => "foo") end should "have nil email" { assert_equal("foo", @user.email) } end end
RSpecは、いろいろ良いところがあるのですが、すでにあるプロジェクトがTest::Unitで書かれていた場合、それをすべてRSpecに置き換えるのは骨が折れますよね。そういう場合、既存のTest::Unitを使い続けながらもRSpecっぽく書けるのがShouldaのメリットです。
私は実際のプロジェクトで使ったことはありませんが、会社の同僚には「Shouldaのほうが好き」という人もいます。それと、「should validate_uniqueness_of(:title)」といったActiveRecordのバリデーションとほぼ同じようなアサーションのヘルパーがあるのも魅力ですね。
実は私はこの記事を書くにあたって初めてRiotのテストを見てみました。Riotは、いろいろと制約を課す代わりに、テストを速く走らせたり、より簡潔に書くことが可能になるそうです。
context "a new user" do setup do User.new(:email => "foo") end asserts("email address") { topic.email == "foo" } end
インテグレーションテストは、これまでに説明してきたユニットテストの組み合わせをテストするものです。インテグレーションテストの定義は人によって微妙にニュアンスが異なります。先ほどの調査では以下のようにコントローラテストのことをインテグレーションテストと呼んでいますね。
コントローラは、複数のモデルを操作するのを目的としていますから、この意味でインテグレーションテストと見なす考えも確かにありだとは思います。しかし、コントローラテストを書くときにユニットテストとインテグレーションテストの両方を兼ねてしまうと、コントローラとモデルの境界線が曖昧になってしまうのではないか、と個人的に思っています。
よって、私はコントローラテストではコードレビューのときに述べた3つの役割、「適切なオブジェクトを取ってくる」「オブジェクトに対する何らかの操作を指示する」「操作が成功した際と失敗した際のビューの振る舞いを指定する」のみをコントローラテストにユニットテストとして書き、インテグレーションテストは別のフレームワークを使うようにしています。
Cucumberは、インテグレーションテストを自然言語で書くことを可能にしたフレームワークです。下の例を見ると、本当にただの英語だということが見て取れますよね。Cucumberは多言語対応しており、日本語で書くことも可能です。
Feature: Addition In order to avoid silly mistakes As a math idiot I want to be told the sum of two numbers Scenario Outline: Add two numbers Given I have entered into the calculator And I have entered into the calculator When I press Then the result should be on the screen Examples: | input_1 | input_2 | button | output | | 20 | 30 | add | 50 | | 2 | 5 | add | 7 | | 0 | 40 | add | 40 |
Cucumberはテストファイルをfeature、各テストケースをscenarioと読んでいます。featureファイルの最初にはテストの目的やその機能を必要とするエンドユーザーの詳細を書き込むことで「ビジネスに必要な機能を作成するんだ」というのを常に徹底するようにしています。文章の構成はGiven、When、Thenの順に並び、各項目にそれぞれ以下のようなことを指定します。
「実際に自然言語で書かれたテストなんかどうやって走らせるの?」と、ここまでの説明を読んで疑問に思われたでしょうか? 上のシナリオを最初に走らせると以下のような警告文が出てテストは終了します。
[worklista (29df2bb...)]$ rake cucumber (in /Users/makoto/work/tmp/worklista) bundle exec /Users/makoto/.rvm/rubies/ruby-1.9.2-p0/bin/ruby -I "/Users/makoto/.rvm/gems/ruby-1.9.2-p0/gems/cucumber-0.9.4/lib:lib" "/Users/makoto/.rvm/gems/ruby-1.9.2-p0/gems/cucumber-0.9.4/bin/cucumber" --profile default Using the default profile... U---U-- 1 scenario (1 undefined) 0m0.037s You can implement step definitions for undefined steps with these snippets: Given /I have entered (\d+) into the calculator/ do |n| pending # express the regexp above with the code you wish you had end rake aborted!
そこで_step.rbファイルに以下のような正規表現のマッチャーと、実装のRuby文を書いていきます。
Given /I have entered (\d+) into the calculator/ do |n| @calc.push n.to_i end
「このページに移動」「このリンクをクリック」といった基本的なことはCucumberが標準で提供しているので、全てのマッチャーを書く必要はありません。
Cucumberの最大の特徴は、それがブラックボックステストであり、テストのほとんどにおいて実装の詳細を気にしなくても良い点です。「ホームページからリンクAをクリックし、フォームに記入する」といったシナリオを書く際、モデルの詳細やURLのルートの構成などはあまり考えなくて良いので、あくまでアプリの振る舞いのみに集中することができます。
Cucumberのようなインテグレーションテストは良いのだけれど、いちいちマッチャーを書くのは面倒くさい。たとえ自然言語でfeatureを書いたとしても、読むのはどうせプログラマー同士だし……。そういう方は、Steakを使ってみるのも良いかもしれません。
以下がコード例です。
feature "Articles", %q{ In order to have an awesome blog As an author I want to create and manage articles } do scenario "Article index" do Article.create!(:title => 'One') Article.create!(:title => 'Two') visit article_index page.should have_content('One') page.should have_content('Two') end
この場合、「Article.create!(:title => 'One')」の部分にモデルの詳細が現れすぎている気もしますが、Machinistといったファクトリーツール(オブジェクトの雛形を作成するツール)で代用することができるそうです。
今回はテストフレームワークの数々を紹介しました。次回からは、ユニットテストにRSpec、インテグレーションテストにCucumberを使って、Worklistaにテストを実装していきましょう。
Copyright © ITmedia, Inc. All Rights Reserved.
Coding Edge 記事ランキング