Grails をマスターする: Grailsアプリケーションのテスト

バグをつぶして実行可能なドキュメントを作成する

Grails では簡単に、バグがない状態で Web アプリケーションを起動させ、その状態を維持させることができます。おまけに、テスト・コードを利用して常に最新状態の充実した実行可能なドキュメント・セットを作成することもできます。今月は Grails の教祖、Scott Davis が Grails でのテストのコツを伝授します。

Scott Davis, Editor in Chief, AboutGroovy.com

Scott DavisScott Davis は国際的に知られた著者、講演者、そしてソフトウェア開発者です。彼の著書には、『Groovy Recipes: Greasing the Wheels of Java』、『GIS for Web Developers: Adding Where to Your Application』、『The Google Maps API』、『JBoss At Work』などがあります。



2008年 10月 14日

私はテスト駆動開発 (TDD: Test-Driven Development) の熱烈な支持者です。Neal Ford (『The Productive Programmer』の著者) 曰く、「テストされていないコードを書くことは、プロとして無責任なことです」(「参考文献」を参照)。Michael Feathers (『Working Effectively with Legacy Code』の著者) は「レガシー・コード」を、対応するテストが関連付けられていないソフトウェアと定義しています。この定義に含まれている意味は、テストを行わずにコードを作成するのは時代遅れのプラクティスであるということです。私は常々、プロジェクトには 1 ポンドの本番コードにつき、2 ポンドのテスト・コードが必要だと言っています。

Grails をマスターする」では今まで Grails のコア機能を活用することに重点を置いていたため、この連載ではまだ TDD について取り上げたことはありませんでした。インフラストラクチャー・コード (つまり、自分で作成したのではない既存のコードのこと) のテストには多少なりとも価値がありますが、私の場合、実際にテストすることはめったにありません。私は、Grails が POGO (Plain Old Groovy Object) を正しく XML に変換すること、つまり trip.save() を呼び出すとTrip が確実にデータベースに保存されることを信じています。テストが実際に価値ある提案となってくるのは、独自のカスタム・コードを検証する段階です。複雑なアルゴリズムを作成する場合には、1 つもしくはそれ以上の補足的なユニット・テストも準備して、アルゴリズムがその意図通りに機能することを証明しなければなりません。そこで今回の記事では、アプリケーションのテストを簡単に実行できるようにする Grails の機能を説明します。

最初のテストの作成

テストを始めるためにまず、新しいドメイン・クラスを導入します。このクラスに最終的に備わるカスタム機能は、テストを用意しない限り、本番には組み込めません。まず、grails create-domain-class HotelStay と入力してください (リスト 1 を参照)。

この連載について

Grails は、Spring や Hibernate などのよく知られた Java™ 技術に「Convention over Configuration (設定より規約)」といった現代のプラクティスを盛り込んだ最新の Web 開発フレームワークです。Groovy で作成された Grails は既存の Java コードをシームレスに統合するだけでなく、スクリプト言語ならではの柔軟性と動的機能を与えてくれます。Grails を学んだら、Web 開発に対する今までの見方がまったく違ってくるはずです。

リスト 1. HotelStay クラスの作成
$ grails create-domain-class HotelStay

Environment set to development
     [copy] Copying 1 file to /src/trip-planner2/grails-app/domain
Created Domain Class for HotelStay
     [copy] Copying 1 file to /src/trip-planner2/test/integration
Created Tests for HotelStay

リスト 1 を見るとわかるように、Grails は自動的に、grails-app/domain ディレクトリーに空のドメイン・クラスを作成し、さらに test/integration ディレクトリーに空の testSomething() メソッドが含まれる GroovyTestCase クラスも作成します (ユニット・テストと結合テストの違いについての詳細は、この後説明します)。リスト 2 に、空の HotelStay クラスと、このクラスに生成されたテストを記載します。

リスト 2. 空のクラスと生成されたテスト
class HotelStay {
}

class HotelStayTests extends GroovyTestCase {
    void testSomething() {
    }
}

GroovyTestCase は JUnit 3.x ユニット・テストを覆う薄い Groovy のファサードです。JUnit の TestCase について十分理解していれば、GroovyTestCase がどのように機能するかについてもおわかりのはずです。いずれのテスト・ケースも、コードの実行内容をアサートすることによってそのコードをテストします。JUnit にはさまざまなアサーション・メソッド (assertEqualsassertTrueassertNull の他、多数) が組み込まれており、これらのアサーション・メソッドを使用して「このコードはこの内容を実行するはずである」とプログラムで表現することができます。

なぜ JUnit 4.x ではなく 3.x なのか

GroovyTestCase は歴史的な理由から JUnit 3.x の TestCase となっています。2007年1月にリリースされた Groovy 1.0 は、Java 1.4 言語の構文をサポートしていました。Groovy 1.0 は Java 1.4、1.5 および 1.6 JVM で動作しますが、言語レベルでの互換性が明確にされていたのは Java 1.4 に対してです。

次のメジャー・リリースは Groovy 1.5 で、これは 2008年1月にリリースされました。Groovy 1.5 は、Generics、static import、for/in ループ、そしてここでの話題に最も肝心なアノテーションをはじめとする Java 1.5 の言語機能をすべてサポートしますが、Groovy 1.5 は Java 1.4 JVM でも動作します。Groovy 開発チームは、Groovy のすべての 1.x バージョンは Java 1.4 との後方互換性を維持すると誓約しています。Java 1.4 のサポートは、Groovy 2.x シリーズのリリースを機に (おそらく 2009年後半か、または 2010年) 廃止される予定です。

以上の説明が GroovyTestCase でラップされた JUnit のバージョンにどう関わってくるのかと言うと、JUnit 4.x では @test@before@after などのアノテーションを導入しています。これらの新しい機能は興味深いものの、Java 1.4 との後方互換性のために、GroovyTestCase では引き続き JUnit 3.x をベースとしています。

そうはいうものの、自力で JUnit 4.x を使用する分には一向に構いません (「参考文献」に、Groovy サイトの関連資料へのリンクを記載)。また、アノテーションや Java 5 言語機能を使用する別のテスト・フレームワークを導入することも可能です (Groovy で TestNG を使用する例については、「参考文献」を参照)。Groovy は Java プログラミングとバイトコードでの互換性を持つため、Groovy ではどの Java テスト・フレームワークでも使用することができます。

リスト 3 のコードを grails-app/domain/HotelStay.groovy と test/integration/HotelStayTests.groovy に追加してください。

リスト 3. 単純なテスト
class HotelStay{
  String hotel
}

class HotelStayTests extends GroovyTestCase {
  void testSomething(){
    HotelStay hs = new HotelStay(hotel:"Sheraton")
    assertEquals "Sheraton", hs.hotel
  }
}

リスト 3 は、前に述べた、まさにあまり価値のない Grails インフラストラクチャーのテストだということは認識しています。皆さんは、Grails がこのアクションを正しく実行すると信用しているはずなので、これは誤ったテストを作成する例としてはぴったりです。可能な限り単純なテストを作成して、そのテストが実行する様子を監視できるという点で、この記事の目的に役立ちます。

すべてのテストを実行するには grails test-app と入力し、この単一のテストを実行するには grails test-app HotelStay と入力します (「Convention over Configuration (設定より規約)」のおかげで、Tests という接尾辞は除外することができます)。どちらのコマンドを入力するにせよ、コマンド・プロンプトでリスト 4 の出力が表示されます (重要な機能を強調するために、多くの不要な部分は削除していることに注意してください)。

リスト 4. テストの実行による出力
$ grails test-app
Environment set to test

No tests found in test/unit to execute ...

-------------------------------------------------------
Running 1 Integration Test...
Running test HotelStayTests...
                    testSomething...SUCCESS
Integration Tests Completed in 253ms
-------------------------------------------------------

Tests passed. View reports in /src/trip-planner2/test/reports

上記の出力から、4 つの重要なことが行われていることがわかります。

  1. ご覧のように、environmenttest に設定されます。これは、conf/DataSource.groovy ファイルの test ブロックに含まれるデータベース設定が作用することを意味します。
  2. test/unit 内のスクリプトが実行されます。ユニット・テストはまだ 1 つも作成していないので、ユニット・テストが検出されなかったのは当然のことです。
  3. test/integration 内のスクリプトが実行されます。HotelStayTests.groovy スクリプトからの出力を見ると、その横に大きく SUCCESS と示されています。
  4. スクリプトが一連のレポートの配置場所を示します。

Web ブラウザーで /src/trip-planner2/test/reports/html/index.html を開くと、実行されたすべてのテストのレポートが表示されます (図 1 を参照)。

図 1. JUnit の最上位レベルのサマリー・レポート
JUnit の最上位レベルのサマリー・レポート

HotelStayTests リンクをクリックすると、doSomething() テストが表示されます (図 2 を参照)。

図 2. JUnit のクラス・レベルのレポート
JUnit のクラス・レベルのレポート

テストが何らかの理由で失敗すると、コマンド・プロンプト出力と HTML レポート (図 3 を参照) の両方によってテストの失敗が通知されます。

図 3. 失敗した JUnit テスト
失敗した JUnit テスト

作成する価値のある最初のテストの作成

最初の単純なテストが機能するようになったので、今度はそれよりも現実的なテストを作成します。例えば、HotelStay クラスに Date checkInDate checkOut という 2 つのフィールドがあるとします。ユーザーの要件では、toString メソッドの出力は Hilton (Wednesday to Sunday) のようにならなければなりません。日付を正しいフォーマットにするのは、java.text.SimpleDateFormat クラスのおかげで簡単なので、この場合に作成するテストは SimpleDateFormat が正しく機能することを検証するためのものではありません。このテストは、toStringメソッドが期待通りに機能することを検証し、ユーザーの要件を満たしていることを証明するという 2 つの目的を果たすように作成します。

ユニット・テストは実行可能なドキュメントです。

通常、ユーザーの要件は何らかの形のドキュメントとして開発者に渡されます。開発者としての仕事は、ドキュメントという形の要件を動作するソフトウェアに変換することです。

要件ドキュメントに伴う問題は、実際にソフトウェア開発が始まった時点でほぼすぐに、最新の状態ではなくなってしまうことです。要件ドキュメントは、ソフトウェアの進化に合わせて進化する「生きたドキュメント」ではありません。要件ドキュメントの場合、成果物という言葉は完全に説明的なものでしかありません。つまり、このドキュメントにはソフトウェアがどのような処理をすべきかについての当初の構想は記述してありますが、現時点での実装が何をするかについての記述はないのです。

十分に包括的な一連のテストを用意するということは、コードをバグがない状態に保つこと以上の意味があります。その付随的なメリットは、ある種の「実行可能なドキュメント」(つまり、変化しているプロジェクト要件の最新の状況をコードで表したもの) を手にできることです。要件にテストを対応付ければ、それをユーザーと共有して、コードが安定していること、そしてユーザーの要件が満たされていることを両方とも証明することができます。この実行可能なドキュメントを CruiseControl などの継続的インテグレーション・サーバー (テストを継続的に何度も実行するサーバー) と組み合わせることで、新しい機能がソフトウェアの正常な部分にリグレッションをもたらすことがないのを確実にする保護メカニズムなります。

ビヘイビア駆動開発 (BDD: Behavior-Driven Development) では、この実行可能なドキュメントという考えを全面的に受け入れています。例えば Groovy で作成された BDD フレームワーク、easyb では、ユーザーと開発者の両方が読めるようなユーザー要件としてテストを作成することができます (「参考文献」を参照)。Microsoft® Word (一例) を放棄しても構わないという先進的な考えのユーザーを対象にしているとしたら、easyb によって時代遅れの要件ドキュメントはまったく必要なくなり、初めからプロジェクトの要件を実行可能なドキュメントにすることができます。

リスト 5 のコードを HotelStay.groovy と HotelStayTests.groovy に入力してください。

リスト 5. assertToString の使用
import java.text.SimpleDateFormat
class HotelStay {
  String hotel
  Date checkIn
  Date checkOut
   
  String toString(){
    def sdf = new SimpleDateFormat("EEEE")
    "${hotel} (${sdf.format(checkIn)} to ${sdf.format(checkOut)})"
  }  
}


import java.text.SimpleDateFormat
class HotelStayTests extends GroovyTestCase {

    void testSomething(){...}

    void testToString() {
      def h = new HotelStay(hotel:"Hilton")
      def df = new SimpleDateFormat("MM/dd/yyyy")
      h.checkIn = df.parse("10/1/2008")
      h.checkOut = df.parse("10/5/2008")
      println h
      assertToString h, "Hilton (Wednesday to Sunday)"
    }
}

grails test-app と入力して、この 2 つ目のテストにパスすることを確認します。

testToString メソッドは、GroovyTestCase が新しく仲間に入れたアサーション・メソッドの 1 つ、assertToString を使用します。JUnit assertEquals メソッドを使用したとしても確かに同じ結果が得られますが、assertToString のほうが表現力には優れています。テストの目的は、テスト・メソッドの名前と最終アサーションを見れば一目瞭然です (「参考文献」に、assertArrayEqualsassertContainsassertLength をはじめ、GroovyTestCase がサポートするアサーションの全リストへのリンクを記載しています)。


コントローラーとビューの追加

これまでは、HotelStay ドメイン・クラスとの対話はプログラムで行ってきました。ここでリスト 6 に記載する HotelStayController を追加して、Web ブラウザーでもテスト・クラスを操作できるようにします。

リスト 6. HotelStayController のソース
class HotelStayController {
  def scaffold = HotelStay
}

create フォームには、微妙な UI の調整が必要です。デフォルトでは、日付のフィールドには日、月、年、時、分が含まれます (図 4 を参照)。

図 4. デフォルトによる日付と時刻両方の表示
デフォルトによる日付と時刻両方の表示

この場合、日付のフィールドのタイムスタンプの部分は無視しても問題ありません。そこで、grails generate-views HotelStay と入力し、図 5 に示す変更後の UI を作成するため、iews/hotelStay/create.gsp と views/hotelStay/edit.gsp の両方で <g:datePicker> 要素に precision="day" を追加します。

図 5. 日付のみの表示
日付のみの表示

有効に機能する HotelStay がサーブレット・コンテナーで実行されるようになったので、次のセクションに移る準備は万端です。早速、ユニット・テストまたは結合テストのどちらを実行するかを検討してみましょう。


ユニット・テストと結合テストの違い

前に述べたように、Grails では 2 つの基本的なテスト・タイプ、ユニット・テストと結合テストをサポートします。この 2 つに構文上の違いはありません。どちらも GroovyTestCase として作成し、同じアサーションを使用します。この 2 つの違いは、そのテストの意味です。ユニット・テストはクラスを単独でテストするように意図されている一方、結合テストでは稼働中の完全な環境でクラスをテストすることができます。

率直に言って、すべての Grails テストを結合テストとして作成するのでもまったく構いません。Grails のcreate-* コマンドはいずれも対応する結合テストを生成するので、大部分の開発者たちはただ単にそれによって生成された結合テストを使用しています。この後わかるように、テスト内容の大部分は完全な環境が稼働状態になっていることが条件となるので、結合テストはかなり有効なデフォルトと言えます。

コアではない Grails クラスをテストする場合には、ユニット・テストが最適です。ユニット・テストを作成するには、grails create-unit-test MyTestUnit と入力します。テスト・スクリプトは個別のパッケージに作成されるわけではないため、ユニット・テストと結合テストには固有の名前を付ける必要があります。これを怠ると、リスト 7 に記載する不愉快なエラー・メッセージが表示されることになります。

リスト 7. 結合テストとユニット・テストの名前が同じ場合に表示されるエラー・メッセージ
The sources 
/src/trip-planner2/test/integration/HotelStayTests.groovy and 
   /src/trip-planner2/test/unit/HotelStayTests.groovy are 
   containing both a class of the name HotelStayTests.
 @ line 3, column 1.
   class HotelStayTests extends GroovyTestCase {
   ^

1 error

結合テストにはデフォルトで Tests という接尾辞が付くため、ユニット・テストにはすべて UnitTests という接尾辞を付けて、固有の名前になるようにします。


単純な検証エラー・メッセージに対するテストの作成

次のユーザー要件では、Hotel フィールドはブランクのままにすることはできないと規定しています。この要件は、Grails の組み込み検証フレームワークを使えば簡単に対応することができます。リスト 8 に示すように、static constraints ブロックを HotelStay に追加してください。

リスト 8. HotelStay への static constraints ブロックの追加
class HotelStay {
  static constraints = {
    hotel(blank:false)
    checkIn()
    checkOut()
  }
  
  String hotel
  Date checkIn
  Date checkOut
  
  //the rest of the class remains the same
}

grails run-app と入力します。Hotel フィールドに何も入力しないで HotelStay を作成しようとすると、図 6 のエラー・メッセージを受け取ります。

図 6.ブランクのフィールドに対するデフォルトのエラー・メッセージ
ブランクのフィールドに対するデフォルトのエラー・メッセージ

ユーザーはこの機能に満足するに違いありませんが、デフォルトのエラー・メッセージではユーザーを感心させるまでには至りません。そこで、ユーザーの希望内容を少し変更して、Hotel フィールドはブランクのままにできないこと、そしてフィールドがブランクの場合には「Please provide a hotel name」というエラー・メッセージを表示することを要件とします。

カスタム String と同じくらいに単純なものであっても、このようにカスタム・コードを追加する場合には、テストを追加しなければなりません (当然のことながら、カスタム・コードが関係しないとしても、ユーザー要件を満たしていると証明するテストを作成するのが無難です)。

grails-app/i18n/messages.properties を開いて、hotelStay.hotel.blank=Please provide a hotel name を追加してください。ブラウザーでブランクの Hotel フィールドをサブミットすると、このカスタム・メッセージが表示されるはずです (図 7 を参照)。

図 7. カスタム検証エラー・メッセージの表示
カスタム検証エラー・メッセージの表示

HotelStayTests.groovy に新しいテストを追加して、ブランクに対する検証が機能することを確認します (リスト 9 を参照)。

リスト 9. 検証エラーのテスト
class HotelStayTests extends GroovyTestCase {
  void testBlankHotel(){
    def h = new HotelStay(hotel:"")
    assertFalse "there should be errors", h.validate()
    assertTrue "another way to check for errors after you call validate()", h.hasErrors()
  }

  //the rest of the tests remain unchanged
}

前に、生成されるコントローラーではドメイン・クラスごとに save() メソッドが追加されると説明しました。ここでも save() を呼び出すことはできますが、この場合、新しいクラスをデータベースに保存したいわけではありません。検証が機能するかどうかを知りたいだけです。そこで役立つのが validate() メソッドです。このメソッドは、検証が失敗した場合には false を返し、成功した場合には true を返します。

hasErrors() メソッドも、テストには貴重なメソッドです。save() または validate() を呼び出した後、hasErrors() によって、検証エラーがあったかどうかを事後に確認することができます。

リスト 10 は、その他の貴重な検証メソッドをいくつか組み込んだ testBlankHotel() の拡張バージョンです。

リスト 10. 検証エラーの拡張テスト
class HotelStayTests extends GroovyTestCase {
  void testBlankHotel(){
   def h = new HotelStay(hotel:"")
   assertFalse "there should be errors", h.validate()
   assertTrue "another way to check for errors after you call validate()", h.hasErrors()  
  
   println "\nErrors:"
   println h.errors ?: "no errors found"   
     
   def badField = h.errors.getFieldError('hotel') 
   println "\nBadField:"
   println badField ?: "hotel wasn't a bad field"
   assertNotNull "I'm expecting to find an error on the hotel field", badField


   def code = badField?.codes.find {it == 'hotelStay.hotel.blank'} 
   println "\nCode:"
   println code ?: "the blank hotel code wasn't found"
   assertNotNull "the blank hotel field should be the culprit", code
  }
}

クラスが検証に失敗したと確信した場合には、getErrors() メソッド (ここでは、Groovy のショートカットであるゲッター構文のおかげで errors に省略されています) を呼び出して org.springframework.validation.BeanPropertyBindingResult を返すことができます。GORM が Hibernate を覆う薄い Groovy ファサードであるのと同じく、Grails 検証の中身も単なる Spring 検証に過ぎません。

println 呼び出しの結果はコマンドラインではわかりませんが、HTML レポートに表示されます (図 8 を参照)。

図 8. テストによる println 出力の表示
テストによる println 出力の表示

HotelStayTests レポートの右下隅に表示された System.out リンクをクリックしてください。

リスト 10 の愛情を込めて名付けられた Elvis 演算子 (頭を横に傾けてみてください。Elvis のオールバックと 2 つの目が見えませんか?) は、Groovy の 3 項演算子のショートカットです。?: の左側のオブジェクトがヌルの場合、右側の値が代わりに使用されます。

Hotel フィールドを「Holiday Inn」に変更してテストを再度実行してください。今度は HTML レポートに別の Elvis 出力が表示されます (図 9 を参照)。

図 9. テスト出力での Elvis の確認
テスト出力での Elvis の確認

Elvis を確認した後は、忘れずにHotel フィールドをブランクに戻して、壊れたテストを配置したままにしないでください。

checkIncheckOut には他にもまだ検証エラーが表示されますが、心配には及びません。このテストに関する限り、これらの検証エラーについては無視して問題ありません。ただしこれは、エラーの有無をテストするだけでなく、特定のエラーがスローされることを確認しなければならないということの説明にはなります。

私がアサートしているテキストは、カスタム・エラー・メッセージと完全には一致していない点に注目してください。前回 (toString の出力をテストしたとき) はストリングの一致に配慮したのに、どうして今回はそうでないのかと言うと、前回のテストでの核心は toString メソッドの出力であったのに対し、今回は Grails がメッセージを正しくレンダリングすることよりも、検証コードが実行されることを確認することのほうが重要だからです。これはまさしく、テストは科学というよりは芸術であることを証明しています (正確なメッセージ出力を検証するとしたら、Canoo WebTest や ThoughtWorks Selenium などの Web 層テスト・ツールを使っていたはずです)。


カスタム検証の作成とテスト

次に取り組むユーザー要件は、checkOut の日付が checkIn の日付より前にならないことを確実にするというものです。この要件に対処するには、カスタム検証を作成する必要があります。そしてカスタム・コードを作成する場合には、当然そのコードのテストも必要となります。

リスト 11 のカスタム検証コードを static constraints ブロックに追加してください。

リスト 11. カスタム検証
class HotelStay {
  static constraints = {
    hotel(blank:false)
    checkIn()
    checkOut(validator:{val, obj->
      return val.after(obj.checkIn)
    })
  }
  
  //the rest of the class remains the same
}

val 変数は現行のフィールドです。obj 変数は現行の HotelStay インスタンスを表します。Groovy は before() メソッドと after() メソッドをすべての Date オブジェクトに追加するため、この検証では単に after() メソッド呼び出しの結果だけを返します。checkOutcheckIn の後であれば、検証は true を返し、そうでなければ false を返してエラーをトリガーします。

ここで grails run-app と入力して、checkIn の日付より checkOut の日付を前に設定した場合、新しい HotelStay を作成できないことを確認してください (図 10 を参照)。

図 10. デフォルトのカスタム検証エラー・メッセージ
デフォルトのカスタム検証エラー・メッセージ

grails-app/i18n/messages.properties を開き、checkOut フィールドのカスタム検証メッセージとして hotelStay.checkOut.validator.invalid=Sorry, you cannot check out before you check in を追加します。

messages.propertiesファイルを保存した後、誤った形式の HotelStay をもう一度保存してみてください。すると、図 11 に示すエラー・メッセージが表示されます。

図 11.カスタム検証エラー・メッセージ
カスタム検証エラー・メッセージ

今度はリスト 12 に記載するテストを作成します。

リスト 12. カスタム検証のテスト
import java.text.SimpleDateFormat
class HotelStayTests extends GroovyTestCase {
  void testCheckOutIsNotBeforeCheckIn(){
    def h = new HotelStay(hotel:"Radisson")
    def df = new SimpleDateFormat("MM/dd/yyyy")
    h.checkIn = df.parse("10/15/2008")
    h.checkOut = df.parse("10/10/2008")
  
    assertFalse "there should be errors", h.validate()
    def badField = h.errors.getFieldError('checkOut') 
    assertNotNull "I'm expecting to find an error on the checkOut field", badField
    def code = badField?.codes.find {it == 'hotelStay.checkOut.validator.invalid'} 
    assertNotNull "the checkOut field should be the culprit", code                 
  }
}

カスタム TagLibs のテスト

対処しなければならないユーザー要件は残り 1 つとなりました。create ビューと edit ビューの checkIncheckOut からタイムスタンプの部分を削除することには成功しましたが、図 12 を見るとわかるように、list ビューと show ビューの日付形式はまだ誤ったままです。

図 12. 日付の (タイムスタンプが含まれる) デフォルトの Grails 出力
日付の (タイムスタンプが含まれる) デフォルトの Grails 出力

最も簡単な対処方法は、新しい TagLib を定義することです。Grails にはすでに <g:formatDate> タグが定義されているので、この既存のタグを利用することもできますが、独自のカスタム・タグを作成すること自体、極めて簡単な作業です。そこで、<g:customDateFormat> タグを以下のように 2 通りの方法で使用できるようにします。

一方の <g:customDateFormat> タグの形は、Date をラップして、あらゆる有効な SimpleDateFormat パターンを許容するカスタム・フォーマット属性を受け入れるというものです。

<g:customDateFormat format="EEEE">${new Date()}</g:customDateFormat>

最も一般的な使用事例は米国式の「MM/dd/yyyy」フォーマットで日付を返すことなので、他に何も指定しない場合には、この形式をデフォルトに設定することにします。

<g:customDateFormat>${new Date()}</g:customDateFormat>

ユーザーの要件を理解できたところで、リスト 13 に示すように grails create-tag-lib Date と入力して、まったく新しい DateTagLib.groovy ファイルとそれに対応する DateTagLibTests.groovy ファイルを作成してください。

リスト 13. 新しい TagLib の作成
$ grails create-tag-lib Date
[copy] Copying 1 file to /src/trip-planner2/grails-app/taglib
Created TagLib for Date
[copy] Copying 1 file to /src/trip-planner2/test/integration
Created TagLibTests for Date

DateTagLib.groovy ファイルに、リスト 14 のコードを追加します。

リスト 14. カスタム TagLib の作成
import java.text.SimpleDateFormat

class DateTagLib {
  def customDateFormat = {attrs, body ->
    def b = attrs.body ?: body()
    def d = new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").parse(b)
    
    //if no format attribute is supplied, use this
    def pattern = attrs["format"] ?: "MM/dd/yyyy"
    out << new SimpleDateFormat(pattern).format(d)
  }
}

TagLib は属性という形での単純なString値、そしてタグの本体を取り、String を出力ストリームに送信します。このカスタム・タグを使用してフォーマットの指定されていない Date 型のフィールドをラップしようとしていることから、2 つの SimpleDateFormat オブジェクトが必要となります。まず、入力オブジェクトが Date.toString() 呼び出しのデフォルト・フォーマットと一致する String を読み込みます。これを適切な Date オブジェクトに構文解析すると、別のフォーマットの String として渡す 2 つ目の SimpleDateFormat オブジェクトを作成することができます。

list.gsp と show.gsp で、checkIn フィールドと checkOut フィールドを新しく作成した TagLib でラップします (リスト 15 を参照)。

リスト 15. カスタム TagLib の使用
<g:customDateFormat>${fieldValue(bean:hotelStay, field:'checkIn')}</g:customDateFormat>

grails run-app と入力してから、http://localhost:9090/trip/hotelStay/list にアクセスし、カスタム TagLib が使用されていることを確認してください (図 13 を参照)。

図 13. カスタム TagLib を使用した日付の出力
カスタム TagLib を使用した日付の出力

最後にリスト 16 に示すテストを作成して、TagLib が期待通りに機能することを確認します。

リスト 16. カスタム TagLibのテスト
import java.text.SimpleDateFormat

class DateTagLibTests extends GroovyTestCase {
    void testNoFormat() {
      def output = 
         new DateTagLib().customDateFormat(format:null, body:"2008-10-01 00:00:00.0")
      println "\ncustomDateFormat using the default format:"
      println output
      
      assertEquals "was the default format used?", "10/01/2008", output
    }

    void testCustomFormat() {
      def output = 
         new DateTagLib().customDateFormat(format:"EEEE", body:"2008-10-01 00:00:00.0")
      assertEquals "was the custom format used?", "Wednesday", output
    }
}

まとめ

いくつかのテストを用意してみて、Grails コンポーネントをテストするのがどんなに簡単かを実感してもらえたはずです。これからは、引き続きテスト駆動で開発を進めることによって、自分が行っている作業に一層確信が持てるようになります。また、ユーザー要件に合わせたテストを作成すれば、常に最新状態の充実した実行可能なドキュメント・セットを用意できるというメリットも得られます。

次回の記事では、JSON (JavaScript Object Notation) に焦点を当てます。Grails には極めて優れた JSON サポートが初めから備わっています。そこで、コントローラーから JSON を生成する方法、そして生成した JSON を GSP から使用する方法を紹介します。それまでは、Grails を楽しみながらマスターしてください。

参考文献

学ぶために

  • 連載「Grails をマスターする」: この連載の他の記事を読んで、Grails とこのフレームワークを使って実現可能なすべての内容をよく理解してください。
  • Grails: Grails の Web サイトにアクセスしてください。
  • Grails Framework Reference Documentation: Grails のバイブルとなります。
  • Static Typing & Bureaucracy Redux」(Neil Ford、Meme Agora 共著、2007年5月): Neil Ford はこのブログ・エントリーで、動的言語を使うとより多くのテストをより短時間で実行できると言っています。
  • Using JUnit 4 with Groovy: Groovy 1.5+ と Java 5+ を使用している限り、Grails で JUnit 4 を使用できます。ここに記載されているサンプル・コードを参考にしてください。
  • Test'N'Groove: Test'N'Groove は、Groovy と TestNG の統合です。
  • Unit Testing: Groovy ではどのようにして JUnit テストを容易に行えるようにするかを読んでください。サポートされるアサーションを網羅したリストも、ここに記載されています。
  • コード品質を追求する: ビヘイビア駆動開発を舞台にした冒険」(Andrew Glover 著、developerWorks、2007年9月): プログラムの結果ではなく、その振る舞いに焦点を当てた開発方法について詳しく学んでください。
  • Groovy Recipes』: Scott Davis の最新の著作で Groovy と Grails の詳細を学んでください。
  • 連載「実用的な Groovy」: developerWorks のこの連載記事では、実用的な Groovy の使用方法を探り、それぞれの方法をいつ、どんな場合に適用するかを解説しています。
  • Groovy:Groovy の Web サイトで、このプロジェクトの詳細を学んでください。
  • AboutGroovy.com: Groovy に関する最新のニュースと記事へのリンクが記載されています。
  • technology bookstore: この記事で紹介した技術やその他の技術に関する本を参照してください。
  • developerWorks Java technology ゾーン: Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。

製品や技術を入手するために

  • Grails: Grails の最新リリースをダウンロードしてください。
  • easyb: Java プラットフォーム向けビヘイビア駆動開発フレームワーク、easyb をダウンロードしてください。

議論するために

コメント

developerWorks: サイン・イン

必須フィールドは(*)で示されます。


IBM ID が必要ですか?
IBM IDをお忘れですか?


パスワードをお忘れですか?
パスワードの変更

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


お客様が developerWorks に初めてサインインすると、お客様のプロフィールが作成されます。会社名を非表示とする選択を行わない限り、プロフィール内の情報(名前、国/地域や会社名)は公開され、投稿するコンテンツと一緒に表示されますが、いつでもこれらの情報を更新できます。

送信されたすべての情報は安全です。

ディスプレイ・ネームを選択してください



developerWorks に初めてサインインするとプロフィールが作成されますので、その際にディスプレイ・ネームを選択する必要があります。ディスプレイ・ネームは、お客様が developerWorks に投稿するコンテンツと一緒に表示されます。

ディスプレイ・ネームは、3文字から31文字の範囲で指定し、かつ developerWorks コミュニティーでユニークである必要があります。また、プライバシー上の理由でお客様の電子メール・アドレスは使用しないでください。

必須フィールドは(*)で示されます。

3文字から31文字の範囲で指定し

「送信する」をクリックすることにより、お客様は developerWorks のご使用条件に同意したことになります。 ご使用条件を読む

 


送信されたすべての情報は安全です。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=60
Zone=Java technology
ArticleID=350704
ArticleTitle=Grails をマスターする: Grailsアプリケーションのテスト
publish-date=10142008