本文へジャンプ

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


お客様が developerWorks に初めてサインインすると、プロフィールが作成されます。プロフィールで選択した情報は公開されますが、いつでもその情報を編集できます。お客様の姓名(非表示設定にしていない限り)とディスプレイ・ネームは、投稿するコンテンツと一緒に表示されます。

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

  • 閉じる [x]

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

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

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


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

  • 閉じる [x]

Grails をマスターする: Grails によるモック・テスト

ユニット・テストによる効率化

Scott Davis, Founder, ThirstyHead.com
Scott Davis
Scott Davis は国際的に知られた著者、講演者、そしてソフトウェア開発者で、Groovy と Grails の教育を目的とした会社、ThirstyHead.com の創設者でもあります。彼の著書には、『Groovy Recipes: Greasing the Wheels of Java』、『GIS for Web Developers: Adding Where to Your Application』、『The Google Maps API』、『JBoss At Work』などがあります。現在、IBM developerWorks の「Grails をマスターする」と「実用的な Groovy」の 2 本の連載を執筆中です。

概要: 連載「Grails をマスターする」の今回の記事では、Scott Davis が Grails の GrailsUnitTestCase クラスおよび ControllerUnitTestCase クラスに組み込まれたモック生成機能を利用する方法を紹介します。

このシリーズの他の記事を見る

日付:  2009年 10月 20日
レベル:  中級 この記事の原文:  英語
アクティビティー: 4505 ビュー
お気軽にご意見・ご感想をお寄せください: 


今回の記事では、通常は結合テストを使ってテストすることになる Grails 成果物でも、簡単にユニット・テストを行えるようにする GrailsUnitTestCase および ControllerUnitTestCase のモック生成機能について学びます。いずれも Groovy のメタプログラミングのマジックを利用する mockForConstraintsTests()mockDomain()、および mockLogging() の 3 つのメソッドが、ドメイン・クラス、サービス、コントローラーのテストをいとも簡単な作業にしてくれます。

Grails アプリケーションのテスト」では、ユニット・テストと結合テストについて以下のように説明しました。

この連載について

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

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

上記の説明は、記事を作成した時点での最新リリースである Grails 1.0 に基づいています。その後の Grails 1.1 リリースでは、テストのインフラストラクチャーが機能の点で大幅に改善されました。GrailsUnitTestCase クラスとその子クラスの導入により、テストの容易さとプロセスの精巧さに格段の差が出ています。具体的に言うと、これらの新しいテスト・クラスが提供するモック生成機能によって、ユニット・テストを短時間で実行できるようになったと同時に、通常は結合テストで対象となるような機能もテストできるようになりました。図 1 に、Grails 1.1.x での新しいテスト階層を示します。


図 1. Grails 1.1.x での新しいテスト階層

次のセクションで新しいドメイン・クラスとコントローラーを作成すると、GrailsUnitTestCaseControllerUnitTestCase の動作が見えてくるはずです (この記事に記載するサンプルの完全なソース・コードはダウンロードすることができます)。

はじめに

この記事に記載する例に従えるように、まず始めに新しいアプリケーションを作成します。コマンド・プロンプトで、以下の内容を入力してください。

grails create-app testing

testing ディレクトリーに移動して (cd testing)、以下の内容を入力します。

grails create-domain-class User

続いて以下の内容を入力します。

grails create-controller User

リスト 1 のコードを grails-app/domain/User.groovy に追加します。


リスト 1. User ドメイン・クラス

class User {
  String name
  String login
  String password
  String role = "user"

  static constraints = {
    name(blank:false)
    login(unique:true, blank:false)
    password(password:true, minSize:5)
    role(inList:["user", "admin"])
  }

  String toString(){
    "${name} (${role})"
  }
}

grails-app/controller/UserController.groovy のコアとなる振る舞いの scaffold を生成します (リスト 2 を参照)。


リスト 2. UserController クラス

class UserController {
    def scaffold = true
}

これで、基本的なインフラストラクチャーが用意できました。ここからは、いくつかのテストを追加していきます。


GrailsUnitTestCase でのモック生成

テキスト・エディターで test/unit/UserTests.groovy を開きます。このファイル内のコードをリスト 3 に記載します。


リスト 3. UserTests クラス

import grails.test.*

class UserTests extends GrailsUnitTestCase {
    protected void setUp() {
        super.setUp()
    }

    protected void tearDown() {
        super.tearDown()
    }

    void testSomething() {

    }
}

Grails 1.0 では、create-domain-class コマンドによって自動的にスタブ化して作成されたテストは、GroovyTestCase を継承していました。しかしご覧のように、現在の Grails 1.1 では ドメイン・クラスのユニット・テストは GrailsUnitTestCase を継承します。その結果、以前は結合テストが必要だったユニット・テストの機能からモックを生成できる新規メソッドを使用できるようになっています。

具体的には、GrailsUnitTestCase は以下のモック・メソッドを提供します。

  • mockForConstraintsTests()
  • mockDomain()
  • mockLogging()

これらのモック・メソッドがいかに貴重なものであるかを理解するため、まずは意図的に失敗するテストを作成します。それには testSomething() メソッドを testBlank() メソッドに変更します (リスト 4 を参照)。


リスト 4. 失敗するテスト

void testBlank() {
  def user = new User()
  assertFalse user.validate()
}

おそらく読者の皆さんは、このテストがなぜ失敗するのか疑問に思うのではないでしょうか。いくら調べても構文的な問題がないにもかかわらず、このテストが失敗する理由は、ここで実行しているのはユニット・テストだからです。ユニット・テストは単独で実行するように意図されています。つまり、実行中のデータベースもなく、稼動中の Web サーバーもなく、そして最も重要なことに、Grails 関連のメタプログラミングも行われないということです。

リスト 1 に記載した User ドメイン・クラスのソース・コードをもう一度見てみると、明らかに validate() メソッドは定義されていません。このメソッドは (save()list()hasErrors()、そしてその他すべてのお馴染みの GORM (Groovy Object Relational Mapping) メソッドと併せて)、Grails が実行時に動的にドメイン・クラスに追加します。

コマンド・プロンプトで grails test-app と入力して、この失敗するテストを実行すると、リスト 5 に記載する結果が表示されます。


リスト 5. コンソール出力でのテストの失敗結果

$ grails test-app
Environment set to test

Starting unit tests ...
Running tests of type 'unit'
-------------------------------------------------------
Running 2 unit tests...
Running test UserControllerTests...PASSED
Running test UserTests...
                    testBlank...FAILED
Tests Completed in 1434ms ...
-------------------------------------------------------
Tests passed: 1
Tests failed: 1
-------------------------------------------------------

Starting integration tests ...
Running tests of type 'integration'
No tests found in test/integration to execute ...

Tests FAILED - view reports in /testing/test/reports.

このあとテストの失敗レポートについて調べますが、その前に、ユニット・テストが極めて短時間で実行されたこと、そして結合テストの実行には顕著な遅延があったことに気付きましたか? grails test-app -unit と入力してユニット・テストだけを実行すると、テストは相変わらず失敗するものの、テストの実行時間には大幅な改善が見られるはずです。

もちろん、grails test-app -integration と入力すれば結合テストだけを実行することができます。実のところ、ユニット・テストおよび結合テストのフラグをテスト・クラスの名前に結び付けることもできます。関心のある特定のテスト・クラスを対象にするには、grails test-app -unit User と入力します (クラス名の接尾辞 Tests は入力しないことに注意してください。どんな場合でも、入力の手間が減ることは良いことです)。このようにテストの実行対象を 1 つのクラスだけに絞り込めるとしたら、実際にテストを作成する場合でも、断然やる気が違ってくるはずです。

テストが失敗するとわかったら、当然エラー・メッセージを見たくなるはずです。それには Web ブラウザーで test/reports/html/index.html にアクセスし、失敗したテスト・クラスをクリックします。すると、図 2 の結果が表示されます。


図 2. 失敗したユニット・テストのレポート

「No signature of method: User.validate()」というエラー・メッセージはまさに、Grails が User クラスに対して validate() メソッドのメタプログラミングを行わなかったことを表しています。

この結果に対して取り得る選択肢は、この時点で 2 つあります。1 つは、このテスト・クラスを integration ディレクトリーに移すことです。けれども Grails が結合テストを実行するために費やす時間を考えると、この選択肢はあまり魅力的ではありません。もう 1 つの選択肢は、検証の振る舞いを模倣したモックを生成し、テストを unit ディレクトリーに残しておくことです。


mockForConstraintsTests() の紹介

ユニット・テストでの Grails 検証のモックを生成するには、mockForConstraintsTests() メソッドを追加します (リスト 6 を参照)。このメソッドによって、Grails はいつものように、実行中に指定のドメイン・クラスに対して検証メソッドのメタプログラミングを行います。


リスト 6. mockForConstraintsTests() によって合格するテスト

void testBlank() {
  mockForConstraintsTests(User)
  def user = new User()
  assertFalse user.validate()
}

テストを実行し、合格することを確認します (リスト 7 を参照)。


リスト 7. 合格するテストの実行

$ grails test-app -unit User
Environment set to test

Starting unit tests ...
Running tests of type 'unit'
-------------------------------------------------------
Running 1 unit test...
Running test UserTests...PASSED
Tests Completed in 635ms ...
-------------------------------------------------------
Tests passed: 1
Tests failed: 0
-------------------------------------------------------

Tests PASSED - view reports in /testing/test/reports.

ユニット・テストをさらに改良するには、検証が特定のフィールドに設けられた特定の制約に対して失敗したことをアサートするという方法があります (リスト 8 を参照)。この場合、mockForConstraintsTests() メソッドはドメイン・クラスに対して errors コレクションのメタプログラミングを行います。この errors コレクションにより、正しい制約がトリガーされたことを簡単に検証できるようになります。


リスト 8. 特定のフィールドに設けられた特定の制約に対する違反のアサート

void testBlank() {
  mockForConstraintsTests(User)
  def user = new User()
  assertFalse user.validate()

  println "=" * 20
  println "Total number of errors:"
  println user.errors.errorCount

  println "=" * 20
  println "Here are all of the errors:"
  println user.errors

  println "=" * 20
  println "Here are the errors individually:"
  user.errors.allErrors.each{
    println it
    println "-" * 20
  }

  assertEquals "blank", user.errors["name"]
}

このテストをもう一度実行すると、失敗に終わるのは予想外でしたか?レポート出力 (図 3 を参照) を調べて、根本原因を突き止めてください。


図 3. blank ではなく nullable であることが原因の失敗

エラー・メッセージは、「expected:<[blank]> but was:<[nullable]>」となっています。このように検証は失敗しましたが、その理由は想像していたものとは異なります。

これは陥りやすいエラーです。デフォルトでは、Grails のドメイン・クラスのフィールドはいずれも null にすることはできません。この暗黙の制約によって問題となるのは、Grails との対話には通常 HTML フォームを使用することです。HTML フォームで String フィールドをブランクのままにしておくと、コントローラーに返される params Map には null でなく、空の String (つまり、"") として含まれることになります。

上記の HTML レポートの下端にある System.out リンクをクリックすると、3 つの String フィールド (nameloginpassword) のすべてが nullable' 制約違反をスローしていることがわかります。図 4 に、println 呼び出しによる出力を示します。暗黙の nullable 制約を満たしているフィールドは唯一、user をデフォルト値に持つ role フィールドだけです。


図 4. テストの System.out 出力

リスト 9 に示すようにもう一度 testBlank() テストを調整して、今度は適切な理由で検証が失敗するようにします (したがって、ユニット・テストは合格するということです)。


リスト 9. 正当な理由で合格するテスト

void testBlank() {
  mockForConstraintsTests(User)
  def user = new User(name:",
                      login:"admin",
                      password:"wordpass")
  assertFalse user.validate()
  assertEquals 1, user.errors.errorCount
  assertEquals "blank", user.errors["name"]
}

テストを再度実行して合格することを確かめたら、今度はもう少し困難な制約に取り組みます。それは、unique 制約です。


mockForConstraintsTests() による unique 制約のテスト

前のセクションで説明したように、ほとんどの制約のテストは単独で簡単に行えます。例えば、password フィールドの minSize5 以上であるかどうかをテストするのは簡単です。このフィールドが依存するのはこのフィールドの値そのものだけで、他の何にも依存しないからです。リスト 10 に、testPassword() メソッドを記載します。


リスト 10. minSize 制約のテスト

void testPassword() {
  mockForConstraintsTests(User)
  def user = new User(password:"foo")
  assertFalse user.validate()
  assertEquals "minSize", user.errors["password"]
}

一方、データベース・テーブルに重複する値が格納されないようにするための unique 制約のような制約をテストするにはどうしたらよいでしょうか。この場合、ありがたいことに mockForConstraintsTests() は 2 番目の引数として、ドメイン・クラスのリストを取ります。このリストを使えば、実際のデータベース・テーブルの代わりとなるモックを生成することができます。リスト 11 に、モック化されたテーブルを使って unique 制約をテストする方法を説明します。


リスト 11. モック化されたテーブルを使用した unique 制約のテスト

void testUniqueLogin(){
  def jdoe = new User(name:"John Doe",
                      login:"jdoe",
                      password:"password")

  def suziq = new User(name:"Suzi Q",
                       login:"suziq",
                       password:"wordpass")

  mockForConstraintsTests(User, [jdoe, suziq])

  def jane = new User(login:"jdoe")
  assertFalse jane.validate()
  assertEquals "unique", jane.errors["login"]
}

データベース・テーブルのモックをメモリー内に生成できるということは、実際のデータベースを起動する時間を考えるとなおさらのこと、大幅な時間の節約となります。実際のデータベースを使うとなったら、データベースが稼動中になった時点で、アサーションが合格するために必要なレコードがデータベース・テーブルに取り込まれている状態でなければならないという難題にも対処しなければなりません。

本番データベースで真の結合テストを実行することが時間の無駄だと言っているわけではありません。長時間実行される結合テストのほうが、継続的インテグレーション・サーバーには適していますが、データベースとのやりとりのモックを生成することで、作業時間を実際にかかる時間の何分の一かに短縮できるため、その分 Grails の機能に集中できるということです。

データベース・テーブルのモック生成機能は、mockForConstraintsTests() メソッドだけに適用されるわけではありません。mockDomain() メソッドでも同じ機能を使用することができます。


mockDomain() の紹介

GORM はドメイン・クラスに対して数々の有用なメソッドのメタプログラミングを行います。これらのメソッドには save()list()、そして findAllByRole() をはじめとする多数の動的な検索メソッドも含まれます。その名前が表すように、mockForConstraintsTests() メソッドはテスト目的で検証メソッドをドメイン・クラスに追加する一方、mockDomain() メソッドはテスト目的で永続化メソッドをドメイン・クラスに追加します。リスト 12 に、mockDomain() メソッドの使い方を記載します。


リスト 12. mockDomain() を使用した GORM メソッドのテスト

void testMockDomain(){
  def jdoe = new User(name:"John Doe", role:"user")
  def suziq = new User(name:"Suzi Q", role:"admin")
  def jsmith = new User(name:"Jane Smith", role:"user")

  mockDomain(User, [jdoe, suziq, jsmith])

  //dynamic finder
  def list = User.findAllByRole("admin")
  assertEquals 1, list.size()

  //NOTE: criteria, Hibernate Query Language (HQL)
  //      and Query By Example (QBE) are not supported
}

mockDomain() メソッドは GORM の振る舞いを可能な限り忠実にモデリングします。例えば、ドメイン・クラスをモック・テーブルに保存すると、実際のアプリケーションと同様に id フィールドに値が取り込まれます。id の値はリスト内の要素の序数にすぎません。リスト 13 に、ユニット・テストでドメイン・クラスを保存する方法を説明します。


リスト 13. ユニット・テストでのドメイン・クラスの保存

void testMockGorm(){
  def jdoe = new User(name:"John Doe", role:"user")
  def suziq = new User(name:"Suzi Q", role:"admin")
  def jsmith = new User(name:"Jane Smith", role:"user")

  mockDomain(User, [jdoe, suziq, jsmith])

  def foo = new User(login:"foo")
  foo.name = "Bubba"
  foo.role = "user"
  foo.password = "password"
  foo.save()
  assertEquals 4, foo.id  //NOTE: id gets assigned
  assertEquals 3, User.findAllByRole("user").size()
}

モック生成とメタプログラミングに関する制約

mockDomain() メソッドはベースになっている Groovy 言語に備わっている動的機能をそのまま利用します (Groovy でのメタプログラミングについて詳しく学ぶには、「実用的な Groovy: クロージャー、ExpandoMetaClass、そしてカテゴリーによるメタプログラミング」を参照してください)。実質的には、通常ドメイン・クラスで行われるはずのメソッド呼び出しをインターセプトし、テスト目的でモック化された振る舞いで置き換えます。つまり当然のことながら、他のサポート技術 (基準ブロック、Hibernate Query Language (HQL)、Query By Example (QBE) など) のモックは生成することができません。コードがこれらの技術のいずれかに依存している場合には、結合テストを作成して実際のデータベースを稼動させる必要があります。

GrailsUnitTestCase では、実際のデータベースのモックを生成できるだけでなく、ロギング・インフラストラクチャーのモックを生成することもできます。


mockLogging() の紹介

GrailsUnitTestCase が役に立つのは、ドメイン・クラスのテストだけに限りません。grails create-service Admin と入力して、Admin サービスを作成してください (リスト 14 を参照)。


リスト 14. サービスの作成

$ grails create-service Admin

Created Service for Admin
Created Tests for Admin

当然、このAdminService.groovy ファイルは grails-app/services ディレクトリーに作成されますが、test/unit ディレクトリーを調べてみると、AdminServiceTests.groovy という名前の GrailsUnitTestCase も生成されていることがわかります。

AdminService に、admin ロールのユーザーだけにサーバーの再起動を許可する架空のメソッドを追加します (リスト 15 を参照)。


リスト 15. AdminService への restart() メソッドの追加

class AdminService {
  boolean transactional = true

  def restartServer(User user) {
    if(user.role == "admin"){
      //restart the server
      return true
    }else{
      log.info "Ha! ${user.name} thinks s/he is an admin..."
      return false
    }
  }
}

このサービスをテストするのは至って簡単なはずで、testRestartServer() メソッドを test/unit/AdminServiceTests.groovy に追加するだけでよいのです (リスト 16 を参照)。


リスト 16. 失敗するサービス・テスト

void testRestartServer() {
  def jdoe = new User(name:"John Doe", role:"user")
  def suziq = new User(name:"Suzi Q", role:"admin")

  //NOTE: no DI in unit tests
  def adminService = new AdminService()
  assertTrue adminService.restartServer(suziq)
  assertFalse adminService.restartServer(jdoe)
}

コマンド・プロンプトで grails test-app -unit AdminService と入力してテストを実行すると、このテストは失敗します。最初の User テストの実行と同じく、失敗する理由は当初予想したものとは違うはずです。HTML レポートを見てみると、お馴染みの「No such property: log for class: AdminService」というメッセージが目に飛び込んできます (図 5 を参照)。


図 5. 依存性が注入されていないことによるユニット・テストの失敗

しかし今回の失敗は、ドメイン・クラスでメタプログラミングが行われていないことが理由ではありません。今回失敗したのは、依存性が注入されていないことによるものです。具体的には、すべての Grails 成果物には実行時に log オブジェクトが注入されます。これによって、メッセージを容易にログに記録できるようにしており、後で調べられるようにしています。

テスト用のモック・ロガーを注入するため、AdminService クラスを mockLogging() メソッド呼び出しでラップしてください (リスト 17 を参照)。


リスト 17. mockLogging() を追加したおかげで合格するサービス・テスト

void testRestartServer() {
  def jdoe = new User(name:"John Doe", role:"user")
  def suziq = new User(name:"Suzi Q", role:"admin")

  mockLogging(AdminService)
  def adminService = new AdminService()
  assertTrue adminService.restartServer(suziq)
  assertFalse adminService.restartServer(jdoe)
}

今度は期待どおりテストは合格し、すべてのログ出力が System.out に送信されます。この出力は、HTML レポートで確認することができます。


ControllerUnitTestCase の紹介

ドメイン・クラスとサービスは GrailsUnitTestCase によって簡単にテストすることができますが、コントローラーをテストするとなると、機能を追加する必要が出てきます。ControllerUnitTestCaseGrailsUnitTestCase を継承することから、前と同じく引き続き mockForConstraintsTests()mockDomain()mockLogging() を使用することができます。また、ControllerUnitTestCase はテスト対象のコントローラーの新規インスタンスを作成し、controller というふさわしい名前の変数に、新しく作成したインスタンスを保存します。この controller 変数が、テスト中にプログラムによってコントローラーとやりとりする手段となります。

コアとなるコントローラーの機能を可視化しやすくするために、コマンド・プロンプトで grails generate-controller User と入力してください。これにより、def scaffold = true がコントローラー・コードの完全な実装で置き換えられます。

完全に実装された grails-app/controllers/UserController.groovy ファイルから、index アクションを呼び出すと list アクションにリダイレクトされることがわかります (リスト 18 を参照)。


リスト 18. UserController でのデフォルト index アクション

class UserController {

    def index = { redirect(action:list,params:params) }

}

リダイレクトが正常に行われることを確認するため、testIndex() メソッドを test/unit/UserControllerTests.groovy に追加します (リスト 19 を参照)。


リスト 19. デフォルト index アクションのテスト

import grails.test.*

class UserControllerTests extends ControllerUnitTestCase {
    void testIndex() {
      controller.index()
      assertEquals controller.list, controller.redirectArgs["action"]
    }
}

ご覧のように、まず始めに、まるでコントローラーでのメソッド呼び出しであるかのようにコントローラーのアクションを呼び出しています。redirect 引数は redirectArgs という名前の Map に保存されます。そしてアサーションによって、action キーに list の値が含まれることを確認します (アクションが render で終わっている場合は、renderArgs という名前の Map に対してアサートすることができます)。

今度は、これよりも多少高度な index アクションで、User のセッションをチェックし、そのユーザーが admin であるかどうかに基づいてリダイレクトする場合を考えてみてください。ControllerUnitTestCase では、sessionflash は両方とも Map なので、呼び出しの前に値を取り込むことも、呼び出しの後でアサートすることもできます。index アクションをリスト 20 のように変更してください。


リスト 20. 高度な index アクション

def index = {
  if(session?.user?.role == "admin"){
    redirect(action:list,params:params)
  }else{
    flash.message = "Sorry, you are not authorized to view this list."
    redirect(controller:"home", action:index)
  }
}

UserControllerTests.groovy 内の testIndex() メソッドに、この新しい機能をテストするための変更を加えます (リスト 21 を参照)。


リスト 21. session 値と flash 値のテスト

void testIndex() {
  def jdoe = new User(name:"John Doe", role:"user")
  def suziq = new User(name:"Suzi Q", role:"admin")

  controller.session.user = jdoe
  controller.index()
  assertEquals "home", controller.redirectArgs["controller"]
  assertTrue controller.flash.message.startsWith("Sorry")

  controller.session.user = suziq
  controller.index()
  assertEquals controller.list, controller.redirectArgs["action"]
}

コントローラーの一部のアクションは入力パラメーターを要求します。ControllerUnitTestCase では、flashsession の場合と同じように params Map に値を追加することができます。リスト 22 にデフォルトの show アクションを記載します。


リスト 22. デフォルトの show アクション

def show = {
    def userInstance = User.get( params.id )

    if(!userInstance) {
        flash.message = "User not found with id ${params.id}"
        redirect(action:list)
    }
    else { return [ userInstance : userInstance ] }
}

GrailsUnitTestCase での mockDomain() メソッドを思い出してください。ここでもこのメソッドを使用することによって、User テーブルのモックを生成することができます (リスト 23 を参照)。


リスト 23. デフォルトの show アクションのテスト

void testShow() {
  def jdoe = new User(name:"John Doe",
                      login:"jdoe",
                      password:"password",
                      role:"user")

  def suziq = new User(name:"Suzi Q",
                      login:"suziq",
                      password:"wordpass",
                      role:"admin")

  mockDomain(User, [jdoe, suziq])

  controller.params.id = 2

  // this is the HashMap returned by the show action
  def returnMap = controller.show()
  assertEquals "Suzi Q", returnMap.userInstance.name
}


ControllerUnitTestCase による RESTful なWeb サービスのテスト

場合によっては、コントローラーをテストするためには未処理のリクエストとレスポンスにアクセスしなければならないこともあります。ControllerUnitTestCase の場合、リクエストとレスポンスを公開する controller.request オブジェクトと controller.response オブジェクトはそれぞれGrailsMockHttpServletRequestGrailsMockHttpServletResponse です。

Grails をマスターする: RESTful な Grails」で説明している手順に従って RESTful なサービスをセットアップし、「実用的な Groovy: XML を作成し、構文解析し、容易に扱う」に従って結果を構文解析すれば、RESTful な Web サービスをテストするために必要なすべてのものが揃います。

UserController に、リスト 24 に記載する単純な listXml アクションを追加します (grails.converters パッケージをインポートすることをお忘れなく)。


リスト 24. コントローラーでの単純な XML 出力

import grails.converters.*
class UserController {
  def listXml = {
    render User.list() as XML
  }

  // snip...
}

次に、testListXml() メソッドを UserControllerTests.groovy に追加します (リスト 25 を参照)。


リスト 25. XML 出力のテスト

void testListXml() {

  def suziq = new User(name:"Suzi Q",
                      login:"suziq",
                      password:"wordpass",
                      role:"admin")

  mockDomain(User, [suziq])

  controller.listXml()
  def xml = controller.response.contentAsString
  def list = new XmlParser().parseText(xml)
  assertEquals "suziq", list.user.login.text()

  //output
  /*
  <?xml version="1.0" encoding="UTF-8"?>
  <list>
    <user>
      <class>User</class>
      <id>1</id>
      <login>suziq</login>
      <name>Suzi Q</name>
      <password>wordpass</password>
      <role>admin</role>
      <version />
    </user>
  </list>
  */
}

このテストで最初に行うのは、新しい User を作成して suziq 変数に保存することです。次に、User テーブルのモックを生成し、suziq を唯一のレコードとして保存します。

この事前セットアップが完了した後に呼び出すのは listXml() アクションです。このアクションによる結果の XML を String として取得するため、controller.response.contentAsString を呼び出して xml 変数に保存します。

この時点で、未処理の String が用意できたことになります (参照用として、メソッドの最後にある output コメントに、この String の内容を記載しています)。new XmlParser().parseText(xml) を実行すると、ルート要素 (<list>) が groovy.util.Node オブジェクトとして返されます。XML 文書のルート・ノードを取得したら、あとは GPath 式 (例えば list.user.login.text()) を使用して、<login> 要素に期待される値 (この例では suziq) が含まれていることをアサートすることができます。

以上のように、Grails の converters パッケージによって簡単に XML を生成し、ネイティブ Groovy ライブラリー XmlParser によって簡単に XML を構文解析し、そして結果として返された GrailsMockHttpServletResponseControllerUnitTestCase によって簡単にテストすることができます。これは、ほんのわずかな行のコードでテストを実行できる、極めて強力な技術の組み合わせです。


まとめ

この記事では、新しく導入された GrailsUnitTestCaseControllerUnitTestCase というテスト・クラスによって、Grails アプリケーションのテストをいとも簡単に行えることを学びました。mockForConstraintsTests()mockDomain()、および mockLogging() メソッドではいずれも、かなり時間のかかる結合テストの代わりに短時間のユニット・テストを生成できるため、作業時間が劇的に短縮されることになります。

次回は、結合テストの厄介さを軽減するコミュニティー主導のテスト用プラグインを紹介します。それまでは、楽しみながら Grails をマスターしてください。



ダウンロード

内容ファイル名サイズダウンロード形式
Source codej-grails10209.zip164KBHTTP

ダウンロード形式について


参考文献

学ぶために

  • 連載「Grails をマスターする」: この連載の他の記事を読んで、Grails とこのフレームワークを使って実現可能なすべての内容をよく理解してください。

  • Grails: Grails の Web サイトにアクセスしてください。

  • Grails 1.1 Release Notes: Grails 1.1.x の新しいテスト・フレームワークについて詳しく学んでください。

  • Grails Framework Reference Documentation: Grails のバイブルです。

  • Groovy Recipes』(Scott Davis 著、Pragmatic Programmers、2008 年): Scott Davis の最新の著作で Groovy と Grails の詳細を学んでください。

  • 連載「実用的な Groovy」: developerWorks のこの連載記事では、実用的な Groovy の使用方法を探り、それぞれの方法をいつ、どんな場合に適用するかを解説しています。

  • Groovy: Groovy の Web サイトで、このプロジェクトの詳細を学んでください。

  • AboutGroovy.com: Groovy に関する最新のニュースと記事へのリンクが記載されています。

  • Technology bookstore: この記事で紹介した技術やその他の技術に関する本を参照してください。

  • developerWorks Java technology ゾーン: Java プログラミングのあらゆる側面を網羅した記事が豊富に用意されています。

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

  • Grails: Grails の最新リリースをダウンロードしてください。

議論するために

著者について

Scott Davis

Scott Davis は国際的に知られた著者、講演者、そしてソフトウェア開発者で、Groovy と Grails の教育を目的とした会社、ThirstyHead.com の創設者でもあります。彼の著書には、『Groovy Recipes: Greasing the Wheels of Java』、『GIS for Web Developers: Adding Where to Your Application』、『The Google Maps API』、『JBoss At Work』などがあります。現在、IBM developerWorks の「Grails をマスターする」と「実用的な Groovy」の 2 本の連載を執筆中です。

不正使用の報告のヘルプ

不正使用の報告

ありがとうございます。 このエントリーは、モデレーターの注目フラグが設定されました。


不正使用の報告のヘルプ

不正使用の報告

不正使用の報告の送信に失敗しました。


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=446923
ArticleTitle=Grails をマスターする: Grails によるモック・テスト
publish-date=10202009
author1-email=scott@thirstyhead.com
author1-email-cc=

タグ

Help
このタグで、My developerWorks のすべてのタイプのコンテンツを見つけるために検索フィールドを使用します。

スライダーバーを使用することで、より多く(少なく)タグを表示します。

人気のタグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するトップのタグを表示します。

マイ・タグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するお客様ご自身のタグを表示します。

このタグで、My developerWorks のすべてのタイプのコンテンツを見つけるために検索フィールドを使用します。人気のタグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するトップのタグを表示します。マイ・タグは、この特定のコンテンツ・ゾーン(例えば、Java テクノロジー、Linux や WebSphere など)に対するお客様ご自身のタグを表示します。