テストまわりで残念なことがあるけど,Grailsはやればできる子

Grailsのテストについて,いくつかまとめておく。Grailsが標準で用意しているテスト環境は単体テスト(unit test)と結合テスト(integration test)の2つあって,それぞれこんな違いがある。

  • 単体テスト(unit test)
    • コンテナ外テストなので,幾つか制約事項あり。
    • たとえば,GORMのダイナミックメソッドは,モックで処理する。
  • 結合テスト(integration test)
    • コンテナ内テスト(正しくはJettyを起動しているワケではないみたい)。
    • GORMの制限はないし,データベースも使える。


...ってな違いは実はどうでもよくて,どっちも「grails test-app」で実行するんだけど,これの使い勝手がすこぶる悪く,かつ遅い。そのためテストのリズムが良くない。せっかくテスト環境持ってたり,モック作れたり,TDDを行うには十分な条件を持っているのだけれど,リズム良くテストを行えないって,かなり致命的な欠点だと思うよ。


例を見せよう。


上記の例は「grails test-app」の実行結果で「単体テスト結合テスト→レポート作成」の順で実行する。いわゆるオールテストを行うので,決して早くはない。この例でも単体テストが2つしかないのに全部終わるまで30秒くらいかかってる。ちょっとやってられない遅さだ。
あー,いかんいかん。問題は遅さではないのだよ。一応,オプションでテスト対象の絞り込みは行えるので,早い遅いはそれほど問題視していなかったりする。:-P
オプションの例はこんなの。

*単体テストだけ実行
  > grails test-app -unit
*単体テストだけ実行(レポートを生成しない)
  > grails test-app -unit -no-reports
*結合テストだけ実行
  > grails test-app -integration
*前回失敗したテストだけ実行(ちょっと怪しい)
  > grails test-app -rerun


遅さより,もっと気に入らないのは,コンソールにテスト結果---というよりスタックトレースとかのエラー情報が出てこないことだ。どうやってエラーの詳細を確認するかっていうとHTMLのJUnitレポートでみる!!...つまりテストするたび毎回ブラウザでJUnitレポートみてられるかよって話。:-(
#そーいやMavensurefireもそんな感じだよね。


次に気に入らんのが,結合テスト。1回テスト(grails test-app)するごとにコンテナ落としてくれるなよ。アプリの実行(grails run-app)がホットデプロイでサクサク開発してるのに,結合テストは「コンパイル→コンテナ起動→テスト実行」を繰り返すって,なにか間違っている。:-(
調べてみたら,同じようなこと思っている人がいたんだけど,今の仕組み上,うまく解決できないみたい。
#ここで言っているインタラクティブモード(grails intaractive)は,無いよりマシ程度なもん。
#...どっちか言うと無くてもイイくらいだったりする。:-P
http://jira.codehaus.org/browse/GRAILS-2911


と,こんな具合でGrailsのテストは,とってもリズムが悪い。なまじ道具が揃っているだけに,この勝手の悪さが残念この上なかった。
ついで言うと,それを理由にほとんどテストコード書かないで,ブラウザで直接操作しながらテスト←→開発をしてたんだけど,ちょっと言い訳じみてるね。すんまそん。:-) でも,テストのリズムの善し悪しって,テストコード書くモチベーションに直結するわ。


文句ばっかじゃつまらんので,ここからは,そのフォロー。


まずは単体テストに限って言えば「grails test-app」を使わず,IDEJUnitランナーから実行できるので,スタックトレースがコンソールに出ない云々はIDEJUnitサポート如何で解決できる。
ちなみに,IntelliJで試してみるとこんな感じで,コマンドで実行するより何倍も良い。:-)


ちょっと注意点があって,IntelliJからはunit/integrationも区分け無く「テスト」に属するので,IntelliJからオールテストを実行するとintegration以下のテストも実行されちゃう。解決策としては,unit以外をテストから除外するか,以下のリンク先のようにunit testだけを相手にするTestSuiteを用意するといい。
Code Shark: Running Grails unit tests in Intellij IDEA


あと,WindowsIntelliJを使っている場合は,プロジェクト設定で「IDE Encoding」をUTF-8にしておくこと。


じゃないと,こんな感じのエラーに悩まされるはずだ。

java.lang.NullPointerException
at org.codehaus.groovy.runtime.callsite.PogoMetaClassSite.call(PogoMetaClassSite.java:39)
at org.codehaus.groovy.runtime.callsite.CallSiteArray.defaultCall(CallSiteArray.java:43)
at org.codehaus.groovy.runtime.callsite.AbstractCallSite.call(AbstractCallSite.java:116)


IDEJUnitランナーを使う=Groovyも一旦コンパイルしてクラスファイルを生成しておくってことなので,ファイルエンコードとかいろいろ悪さしていたみたい(groovyファイルはUTF-8で記述してま)。
これ,ちょっとGroovyの本質的な話題でもあって,上手い事Javaの資産(この場合,JUnit)を活用してるんだけど,そのためにJavaの制約(この場合,コンパイル)に巻き込まれるワケ。こうゆう事情を理解できないと,Groovyはイマイチだって思われても仕方ないよね。


そんなワケで,「IntelliJでは」大きな不満もなくテストでけた(ただし単体テストに限る)。他のIDEは正直知らん。:-P


ちょっと横道にそれるけど,JUnit3ベースのGrailsのテスト環境だと,Javaで書いた方が快適だなって思う。記述量はJavaの方が多いけど,確実にコード補完はしてくれるし,Infinitestとかの支援ツールも利用できるかんね。GrailsみたいなLL系だと,もっとDSL風にテストコードを記述できないと得した感じしないね。
#easyb? easybがその答えって気はしないんだけどね。:-)


もう一方の結合テストについては,今んとこ抜本的な解決には至ってない。ただ,このリンク先読んでピーンと来たことはあったので,それを紹介してお茶を濁す
http://naleid.com/blog/2009/06/14/grails-testing-command-line-aliases/

    |
  \ _ /
 _ (m) _
    目   ピコーン
  / `′ \
   ∧_∧
   (・∀・∩
   (つ ノ
   ⊂_ノ
    (_)


要するにだ。スタックトレースが出なきゃ出せばいいんだよ。先の例では,bashでConsole.appにJUnitのログ出してたけど,それを「groovyでコンソールにJUnitのログを出す」にすりゃ良いじゃん。
という事で,$PROJECT_HOME/scripts/Events.groovy にこんなの用意した。

eventTestSuiteEnd = {type, suite ->
  new File(testReportsDir, "plain").eachFileMatch(~/.*Tests\.txt/) { file ->
    file.withReader("UTF-8") {reader ->
      reader.readLine()
      def line = reader.readLine()
      // JUnitのログの2行目を読み込んで,テストが失敗してたら,それをコンソールに吐き出す
      (line =~ /.*, Failures: (\d), Errors: (\d), .*/).each {m0, m1, m2 ->
        if (m1 == "0" && m2 == "0") return
        println "== ${file.name} =============================================="
        println line
        while ((line = reader.readLine()) != null) println line
      }
    }
  }
}


わはは。オレ,超アタマ良い!!スタックトレースさえ出てくれれば,問題位置へのジャンプはIDEがやってくれるので,もう全然OKって感じだ(起動が遅いのは変わらずだけど)。
しかしあれだな,Grails本体をいじくることなくサックリ出来ちゃうって,Grailsの拡張性の良さはちょっとスゴイな。


これだと,テスト完了イベントで起動するので,IntelliJの「Grails Tests Configuration」から結合テストを実行しても,ちゃんと反応する。:-)
IntelliJGrails Tests Supportって,ただgrails test-appIntelliJから実行するだけなんだもん。
#サポート言うんだったら,JUnitランナーに乗せてくれと言いたい。:-P


あと,インタラクティブモードだとコマンド引数がうまく渡せなくて悶々してたんだけど,よく考えれば短縮コマンドを自作すりゃいいんだなと思ってもみた。だから,こんなのも作ってみたよ。:-D
#調子こいてインタラクティブモードで実行しまくったらOut Of Memoryが出た。:-(

=== $PROJECT_HOME/scripts/RunIt.groovy ===
// grails run-it
// grails test-app -integration -no-reports 相当を行う
includeTargets << grailsScript("Init")
includeTargets << grailsScript("_GrailsClean")
includeTargets << grailsScript("_GrailsTest")

target(main: "Run a Grails application integration tests") {
  depends(checkVersion, configureProxy, parseArguments, cleanTestReports)
  phasesToRun << "integration"
  createTestReports = false
  allTests()
}

setDefaultTarget(main)


ps.
開発ほとんど終わってから,Grailsのテスト環境もいろいろ工夫できるんだなってオモタ(後の祭りだけど)。大分理解は進んだけど,もひとつ何か決定だが欲しいナー。どうも単体テストのモックは,何テストしたらいいの?と思えて仕方が無い。:-(
Effective Testing on Grails | SkillsCast | 1st October 2008