2009年5月27日水曜日

ANTLRWorksがうまく動くようになった

以前、ANTLRWorksがまともに動作しねぇ!というエントリを書いたんだけど、なんとかうまく動作するようになった。"Bundle for Mac OS X"とかいうバイナリをダウンロードして使っていたけど、jarでの配布もあったのでそれをもってきて"-Xmx768m"くらいで起動する。以下のようにANTLRWorksの設定画面でオプションをいくつか指定すればOKっぽい。

オプションの一覧は以下のWikiに書いてあった。自分は"-Xconversiontimeout 10000 -Xmultithreaded"を指定して様子をみている。相変わらず糞遅いのは変わりないんだけど、どーしてもANTLRWorks上でデバッグしたい時にはなんとか現実的な時間(解析とソースのジェネレートで1分近くかかるけど)で動いてくれるのでありがたい。

ちなみに、ルールの単体テストをするためのドキュメントも見つけた。こういうのも書いてくれているのは助かるなぁ。ANTLRではANTLRWorkのような専用のIDEやこのTDDドキュメント、さらにmavenプラグインも提供されていたり、さらにさらにHudsonでのCIが行われていたり、としっかりやっているのがスゴイなぁ。しっかりしているなぁ。

2009年5月26日火曜日

やっとReleaseIt!を読み終えた

今更かよ、といったくらい時間がかかってしまったけれども、やっと読み終わった。言い訳させてもらうと、途中で嫁さんに貸したからです(嫁さんの会社では運用もやっているので、自分よりも先に読んだ方が良いだろうと思ったから)。

読んでみて最初の感想は、この本をスルーするか迷っていた自分に「正座して読むべき」というコメントを下さったかくたにさんに感謝すべきだと思った。これはスルーして良い本ではない。

色んな箇所で「あっ!?」と気づいたり「あ〜!?」と反省したりする点が多く、どの章がどうとかいぅ細かい感想は書けないカンジ。

「第I部:安定性」「第II部:処理能力」は「ケーススタディ->説明->アンチパターン->パターン」という流れでの説明で、リズムも良く読みやすい。やはりアンチパターンの章が勉強になるし、結構踏んできている自分が悲しくなったりしてとても良い。

「第III部:設計における一般的な問題」は専門的な知識が必要な話も多く難しかったので、あんまり吸収できなかった…。

「第IV部:運用」は運用の話で、自分が運用に携わっていたのは随分むかしになるのだが、それでも身にしみて色々と勉強になった。特に読んでいて勉強になるというか、自分も意識していた内容として「17章:透明性」をオススメしておきたい。この章はとても良い。

全体的に見ると「ここまでやってられるかよ」と感じる内容も多いが、バランスも意識して書かれている。「どこまでやろうか」というバランスは今までも重要だったけど、この本を読んでからはそのバランスを取る位置が確実に変わった。なぜかというと、今までだと「コストが高くなる」と思っていた事を実現するにも、あの手この手色々あるという事がわかって、工夫次第でそんなにコストを書けずに色んな策を施せるという事がわかったし、自分が如何に安易にアンチパターンを踏んでいたかがわかったからw

システム構築に携わっている人は是非読むと良いと思います。以下はかくたにさんからのコメントの引用。

『Release It!』はソフトウェア開発に関わる者は全員、正座して読むべき1冊だと思います。フェーズとかそういう問題じゃないんです ;-)
ちなみに、かくたにさんは18章の"適応"を超おすすめしておられた。

2009年5月23日土曜日

MacbookAir: ビルド中とか、再起動中とか

cpuに負担がかかりそうで、しばらく放置してもいい状況とかではこんなカンジにしておく。もぅこれしかねぇ><

しばらくしてから出してくると、大概の処理は終わってるし短時間でもヒンヤリしてイイヨ!

追記

中々具合が良いんだけど、長い事入れっぱなしにするとディスプレイやタッチパッドが曇るのがちょっとした難点だ。

GAE/JでJDOの永続化をフックする

表題の事をできる仕組みがGoogle Cookbook - Google App Engineに紹介されていた。Google App Engineのデータストアというより、JDO標準の機能のようだ。Cookbookでは「Created/Updated Timestamps」という内容で説明されている。Entityが永続化されるタイミングで作成日時や更新日時を設定する、といった事ですね。

InstanceLifecycleListener

JDOの仕様にはInstaceLifecycleListenerというリスナがあり、これらのサブクラスにはいくつか種類がある。今回はこの中のStoreLifecycleListenerというリスナを使用する。

準備

まずはStoreLifecycleListenerの実装と、Entityを拡張するためのインターフェースを作成する。

各メソッドの引数で渡されるInstanceLifecycleEventクラスの getPersistentInstance()メソッドで永続化される対象のオブジェクトを取得する事ができるので、Entityを拡張するインターフェースにキャストして、必要なメソッドを実行するカンジ。

拡張対象のEntityを拡張する

上記で言うと、StoreHookAdapterで拡張するかんじ。

使ってみる

ポイントはPersistenceManager#addInstanceLifecycleListener()で、こいつでInstanceLifecycleListenerの実装と、Entityへの拡張を行うインターフェースを関連づける。これは「第2引数で渡した拡張が行われたEntityの永続化のタイミングで、第一引数で渡したInstanceLifecycleListenerの実装を実行する」という意味となるので、拡張が行われていないEntityを永続化した場合にはInstanceLifecycleListenerの実装は走ってくれない(通知されない)。なんかツマランなぁ、と思って pm.addInstanceLifecycleListener(new StoreHookListener(), Object.class); とやってみたら、全てのEntityに対してフックが適用できたw はい、文句ありません。

2009年5月19日火曜日

antlr3-maven-plugin

ANTLRWorksが便利なんだけど、grammarファイルが大きいのか、ソースのジェネレートがうまく動作してくれない。

error(10): internal error: org.antlr.tool.Grammar.createLookaheadDFA(Grammar.java:1242): could not even do k=1 for decision XXX; reason timed out(>1000ms)

上記のようなメッセージがいくつか出て、cpuをひとつ使い切ったまま処理が返ってこなくなってしまい、強制終了するしかなくなる(少なくとも分単位では落ち着いてくれない)。$ java -cp antlr-3.1.3.jar ...とやってもいいんだけど、せっかくだからmavenでできないか?と思ったら、mavenのプラグインも提供されていた。知らなかった。

antlr3-maven-plugin

grammarファイルは"src/main/antlr3"配下に配置するのがお約束のようだ。ただし、src/main/antlr3直下に置くとマズイようで、適当なフォルダ(Lexer,Parserが出力されるべきパッケージ構成を作成する)に配置するのが良いみたい。

Mavenでは問題無くソースがジェネレートできているので、ANTLRWorksは編集用にしか使っていないカンジ。

2009年5月16日土曜日

GAE/J用のarchetypeプラグインを作成した

やっぱ依存関係とかはmavenを使わないと面倒だ。依存ライブラリのソースの添付とかも簡単にできるし。そんなワケでなんとかmavenとeclipseを共存させられないか、と四苦八苦していたのだがよぅやくなんとかなったっぽい。といぅわけでarchetypeプラグインを作ってみた。

使い方

まずはmaven的にプロジェクトを作成する
artifactを作成するフォルダに移動して以下のコマンドを実行する。
$ mvn archetype:create -DarchetypeGroupId=com.shin1ogawa -DarchetypeArtifactId=gae-jdo-quickstart -DarchetypeVersion=1.0-SNAPSHOT -DremoteRepositories=http://gae-j-samples.sourceforge.jp/maven/repository -DgroupId=com.yourdomain -DartifactId=yourArtifactId
強調斜体で表記した部分は適宜変更してください。必要であれば作成するartifactのversionやらpackageを追加で指定すればいいと思う。
テストケースだけ実行してみる?
作成したartifactのフォルダに移動して以下のコマンドを実行する。
$ mvn clean test
mavenからのテストケースも実行できるはず。
eclipse用に構成する
作成したartifactのフォルダに移動して以下のコマンドを実行する。
$ mvn eclipse:eclipse dependency:copy-dependencies
依存モジュールをコピーしているのは、eclipseからGAE SDKでwebアプリとして起動する時に必要なjarを/war/WEB-INF/libにコピーする必要があるからです。
eclipseに取り込もう
eclipseを起動して、[Import][Existing Projects...]で作成したartifactのフォルダを選択するだけ。
注意!eclipseの設定で、"M2_REPO"がローカルリポジトリをさしている必要がありまっす。これはeclipseの設定画面を開いて、[Java][Build path][Classpath variables]で追加できます。[Name]に"M2_REPO"、[Path]に例えば"/Users/shin1/.m2/repository"みたいなかんじ。普段からmavenとeclipseを使っている人なら設定済みじゃないかと。
Webアプリを実行する
これはeclipseからしか実行できません。プロジェクト直下の"launches/gaej.web.launch"を右クリックして、"Run As"から実行してください。
eclipseのコンソールに”The server is running at http://localhost:8080/”と表示されたら正常に起動できてます。ブラウザからhttp://localhost:8080/にリクエストしてみると、簡単なゲストブックが動作するはず。
eclipseからテストケースを実行してみる
プロジェクト直下の"launches/gaej.unittest.launch"を右クリックして、"Run As"から実行してください。

作成される雛形について

  • プラグイン名を見てもわかるとおり、jdoを使う雛形が作成されます。
  • Viewには素のjspを使ってます。
  • memcacheを少し使ってます。
  • mavenからunit testを実行できるので、CIするのも簡単です。
  • eclipseからunit testを実行できるので、リズムよく開発できます。

何か問題があれば、TwitterなりFriendFeedなりで連絡をくださいー。

追記

以下が前提です!

  • mavenがインストールされている。
  • EclipseにGoogle App Engine SDK Pluginがインストールされていて、1.2.1にアップデート済みである。

mavenはいいとして、eclipse pluginについて初期の1.2.0を入れてしまっている人は以下の手順でUpdateする事ができます。

  1. [Help][Software Updates]メニューを開く
  2. [Google Update Site for Eclipse 3.4]にチェックを入れる。
  3. [Install]ボタンを押す。
  4. 終わるまで待つ。再起動を促されたら素直に再起動する。
  5. Eclipseの設定画面を開いて[Google][App Engine]を開くと、"1.2.0"と"1.2.1"があるので、"1.2.1"をチェックする(デフォルトにする)。

このあたりのGoogleAppEnginePluginのバージョンやらPluginを導入するタイミングによっては、以下のようなExceptionが出ます。

java.lang.NullPointerException
 at com.yourdomain.logic.AbstractLogic.newQuery(AbstractLogic.java:29)
...
Caused by: org.datanucleus.exceptions.NucleusException: Plugin (Bundle) "org.datanucleus.store.appengine" is already registered. Ensure you dont have multiple JAR versions of the same plugin in the classpath. The URL "file:/Users/shin1/Documents/projects.gae/yourArtifactId/war/WEB-INF/lib/datanucleus-appengine-1.0.1.jar" is already registered, and you are trying to register an identical plugin located at URL "file:/Users/shin1/Documents/projects.gae/yourArtifactId/war/WEB-INF/lib/datanucleus-appengine-1.0.1.final.jar."

この場合はプロジェクトを作り直すか、war/WEB-INF/lib/datanucleus-appengine-1.0.1.final.jarを消してやってください(プロジェクトを作り直した方が無難)。

2009年5月15日金曜日

mvnsearch上のGAE/J関連の1.2.0から1.2.1への移行での変更

先のエントリでmvnsearchリポジトリも1.2.1への対応が云々書いたけど、なんかartifactIdがビミョーに見覚えあったり無かったりで「あれ?」てカンジだった。なのでよく確認してみると、結構な差があった。gae-mvn-archetypeプロジェクト/trunk/gae-mvn-archetype/src/main/resources/archetype-resources/pom.xmlのr33での変更内容を見るとよくわかるようだ。

ただしこのpom.xmlだとmaven-datanucleus-pluginの設定がされてないから、エンハンスもmavenでやるとすると以下のようになる。

mvnsearchのGAE/J関連モジュールも1.2.1が追加されてる

Google App Engine for Java SDKの1.2.1がリリースされたけど、mvnsearchのリポジトリも追随されたようです。

  • http://mvnsearch.org/maven2/com/google/appengine/appengine-api-1.0-sdk/
  • http://mvnsearch.org/maven2/com/google/appengine/appengine-api-1.0-stubs/
  • http://mvnsearch.org/maven2/com/google/appengine/appengine-tools-sdk/
  • http://mvnsearch.org/maven2/com/google/appengine/appengine-api-1.0-runtime/

pomのテンプレートもまた書き直さなきゃなぁ。

追記

よく見るとartifactIdが1.2.0の頃と随分違う。詳しくは次のエントリで補足した。

2009年5月14日木曜日

GAE/Jでencoded-pkを使ったEntityをPersistenceManager#deletePersistence()できなくて困っている

GAE/Jではテーブルの親子関係を構成するために、Entityの主キーにcom.google.appengine.api.datastore.Keyを使う事が殆どだと思う。例えば以下に示す"Parent1"クラスのようなカンジ。

Keyオブジェクトが保持する連番をレコードのIDぽく使いたいなーと思って、KeyにencodedStringを使い、KeyのID値も別途保持できるような構成を作ってみた。例えば以下に示す"Parent2"クラスのようなカンジ。Owned relationshipの構成で保持しているChild2クラスも同様にencodedStringを使ったKeyを保持している。

こいつを削除するための処理として、以下のようにPersisteceManager#deletePersistence()を使って記述する。

Parent1は問題無く削除が動作しているが、Parent2の場合にだけExceptionが発生する。

なんでやろー??わかる人教えてください><

ちなみに、DataStoreService#delete()だと問題無く動作するんだけど、こっちはdelete cascadeが走らないんだよなー。

2009年5月11日月曜日

GAE for Java用のサンプルプロジェクトを公開しておきます

シンプルな構成

GAE for Javaでテンプレートとして使えるサンプルを作っていたが、おおよそできた。

  • ORMにはJDOを使う
  • EclipseでのJDOエンハンス対象クラスを設定する
  • Eclipse、MavenのどちらからもJDO関連のモジュールの単体テストが動作する
  • Javaのロガーのフォーマッタを入れ替える(1ログ1行で表示する)
  • Presentation層にはjspとjstlを使う

主にMavenでの単体テストが可能な事がメインで、これができるとHudsonでMavenプロジェクトを定義してのCIが可能となるので必須の機能なはず。みんなCIしてるもんね?CIを想定しなくて許されるのは小学生までだよね?
極力シンプルにしたので、ここから色々派生する事ができると思う。

こいつはシンプルなのでarchetype-pluginを作ってもいいんだけど、要望はなさげ。というのも、$ mvn eclipse:eclipseしてもEclipse側でうまく扱えない→結局ローカルでの実行にantかeclipseが必要になるので、maven単体でIntegrationTestが実行できそーにない。ここまでできればmaven一本で環境構築できて嬉しいんだけどな。cargo-pluginを使ってtomcatにデプロイして動作させる事もできるのかもしれんけど、gaeのsdkとの振る舞いの違いが気になって何かイヤだ。
ちなみにeclipseとmavenをうまく連動する事ができない問題は、eclipse側でmavenでいう依存性のスコープの概念が無いという問題と、jdoのdatanucleusのエンハンスの問題。GoogleAppEnginePlug-inによるエンハンス実行時の環境でのシステムプロパティを触る事ができれば結構うまく連動できそうなんだけども、GoogleAppEnginePlug-inのソースが見当たらないのでなんともならない。

  • https://svn.sourceforge.jp/svnroot/gae-j-samples/gae-jdo-simple-sample/trunk/gae-jdo-simple-sample

t2-frameworkの構成

t2-frameworkを使ったエントリも書いていたが、そちらのサンプルは主に以下のようにしている。

  • ORMにはJDOを使う。
  • EclipseでのJDOエンハンス対象クラスを設定する
  • Eclipse、MavenのどちらからもJDO関連のモジュールの単体テストが動作する
  • Javaのロガーのフォーマッタを入れ替える(1ログ1行で表示する)
  • Presentation層には何も使っていない。jQueryからt2-frameworkの@Ajaxなインターフェースを呼び出して描画してるだけ
  • t2はGuiceアダプタを使用している

t2-frameworkはシンプルさのバランスが絶妙なので、上記のシンプルなサンプルがあればt2はすぐ動作するし、サンプルなんて必要無いくらい。Viewに何のフレームワークも使っていないのは、はt2の@AjaxでWebAPIドリブンぽくGAE/Jを使い、WebブラウザでのViewにはjQueryで描画するだけにしておきたいため。
自分が作るサービスではWebのインターフェースはおまけ程度にして、AIRを正式なクライアント側プラットフォームとして使おうと考えている。なので、shotたん、new AMF()したいです…とかつぶやいておく事にする。

  • https://svn.sourceforge.jp/svnroot/gae-j-samples/gae-jdo-simple-sample/trunk/gae-jdo-simple-sample

Wicketの構成

Wicket on GAE/Jについての最後のエントリ以降、何も触ってませんw

t2のトコでも書いた通り、自分はWebでのインターフェース自体を重視していないためWicketにはあんまりこだわっていない。テンプレートとPageクラスの両方でコンポーネントの構造を意識する必要がある時点でDRYじゃないなーとかマイナスな思考も強くなって来た。Javaからのアプローチをする時はPageクラスさえ構築すれば後はよしなにやってくれろや、とか。
とはいえWicketは大好きなんで、ちょっとしたサービスをGAE/Jにデプロイする際はWicketを使う可能性が一番高いと思う。シンプルなサンプルを作成するためにjstlとか触ったら、htmlに式を書くとかありえなく思えて気絶しそうになったし。

2009年5月10日日曜日

MavenからGAE/Jの単体テスト - Java6でも大丈夫だった

先のエントリで「Java6でdatanucleusのエンハンサが実行できないー!」と叫んでいたけど、単なる凡ミスでした、お騒がせしてすいませんでした。手っ取り早く原因をお伝えしますと、エンハンサがJava5のjavacで走っていました!!

  1. $JAVA_HOMEには確かにJava6を設定してあるし、そもそもJava6のjavacが使われていなかったらmaven-compiler-pluginのsource/targetで"1.6"を指定してある箇所でエラーが出るもんなぁ。mavenは間違いなくJava6で走ってる。なのになぜ…?datanuclesとかあんま耳になじみが無いライブラリを使ってるからこんな事になるんじゃねぇの!?
  2. 念のため"$ echo $JAVA_HOME"してみると、ちゃんとJava6のフォルダが指定されている。
  3. がしかし!"$ javac -version"してみると"javac 1.5.0_16"とか帰って来た!?なるほど、確かにJAVA_HOMEの指定しかしていないしな…。。。mavenはそっちを見てるからJava6で走ってるのは間違いないし、これは関係ないだろ。
  4. pom.xmlを眺めていると、maven-datanucleus-pluginの設定に"fork=true"の設定が!

fork=falseしたら問題無く実行されました…。

エンハンサの動作がうさんくせーとか思ってたから、あーまた行き詰まったよイヤだよホントもぅ、とか思って思考が停止してますた。

2009年5月9日土曜日

MavenからGAE/Jの単体テスト - Java6だとダメ?

前回のエントリで書いた方法だと、Java6のプロジェクトでエンハンスに失敗してしまうよぅだ。。。

どさくさにまぎれてgistからのembbedを試してみた。

追記

pomは前回のエントリとほぼ同じで、javaのバージョンを1.6にしただけ。

2009年5月4日月曜日

MavenからGAE/Jの単体テスト

最低限のpomを記載しておく。ポイントは”datanucleus-appengine"と"datanucleus-core"のscopeをruntimeにしておく事。こいつらがビルド時に取り込まれてしまうとmaven-datanucleus-pluginでのエンハンス中にエラー停止してしまう。

先のエントリで書いた単体試験の内容で、$ mvn clean testしてテストが通る事を確認できるはずだ。

<project xmlns="http://maven.apache.org/POM/4.0.0"
 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>com.shin1ogawa</groupId>
  <artifactId>gae-jdo-simple-sample</artifactId>
  <packaging>war</packaging>
  <version>1.0-SNAPSHOT</version>
  <name>GAE JDO Simple Sample</name>
  <dependencies>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.5</version>
    </dependency>
    <dependency>
      <groupId>javax.persistence</groupId>
      <artifactId>persistence-api</artifactId>
      <version>1.0</version>
    </dependency>
    <dependency>
      <groupId>javax.jdo</groupId>
      <artifactId>jdo2-api</artifactId>
      <version>2.3-SNAPSHOT</version>
    </dependency>
    <dependency>
      <groupId>commons-lang</groupId>
      <artifactId>commons-lang</artifactId>
      <version>2.4</version>
    </dependency>
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.5</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.hamcrest</groupId>
      <artifactId>hamcrest-library</artifactId>
      <version>1.1</version>
      <scope>test</scope>
    </dependency>
    <!-- GAE for java(compile) -->
    <dependency>
      <groupId>com.google.appengine</groupId>
      <artifactId>appengine-api</artifactId>
      <version>1.0-sdk-1.2.0</version>
    </dependency>
    <!-- GAE for java(test) -->
    <dependency>
      <groupId>com.google.appengine</groupId>
      <artifactId>appengine-api-stubs</artifactId>
      <version>1.0</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>com.google.appengine</groupId>
      <artifactId>appengine-local-runtime</artifactId>
      <version>1.0</version>
      <scope>test</scope>
    </dependency>
    <!-- GAE for java(runtime) -->
    <dependency>
      <groupId>com.google.appengine</groupId>
      <artifactId>datanucleus-appengine</artifactId>
      <version>1.0.0.final</version>
      <scope>runtime</scope>
    </dependency>
    <dependency>
      <groupId>org.datanucleus</groupId>
      <artifactId>datanucleus-core</artifactId>
      <version>1.1.0</version>
      <scope>runtime</scope>
    </dependency>
  </dependencies>
  <build>
    <plugins>
      <plugin>
        <groupId>org.apache.maven.plugins</groupId>
        <artifactId>maven-compiler-plugin</artifactId>
        <configuration>
          <source>1.5</source>
          <target>1.5</target>
        </configuration>
      </plugin>
      <plugin>
        <!-- http://www.datanucleus.org/products/accessplatform/enhancer.html#maven2 -->
        <groupId>org.datanucleus</groupId>
        <artifactId>maven-datanucleus-plugin</artifactId>
        <version>1.1.0</version>
        <configuration>
          <verbose>true</verbose>
          <mappingIncludes>**/entity/*.class</mappingIncludes>
          <fork>true</fork>
          <enhancerName>ASM</enhancerName>
          <api>JDO</api>
        </configuration>
        <executions>
          <execution>
            <phase>compile</phase>
            <goals>
              <goal>enhance</goal>
            </goals>
          </execution>
        </executions>
      </plugin>
      <plugin>
        <artifactId>maven-surefire-plugin</artifactId>
        <configuration>
          <excludes>
            <exclude>**/TestEnvironment.java</exclude>
            <exclude>**/Abstract*.java</exclude>
          </excludes>
        </configuration>
      </plugin>
    </plugins>
  </build>
  <repositories>
    <repository>
      <id>DataNucleus</id>
      <url>http://www.datanucleus.org/downloads/maven2</url>
    </repository>
    <repository>
      <id>mvnsearch</id>
      <url>http://www.mvnsearch.org/maven2</url>
    </repository>
  </repositories>
  <pluginRepositories>
    <pluginRepository>
      <id>DataNucleus</id>
      <url>http://www.datanucleus.org/downloads/maven2</url>
    </pluginRepository>
  </pluginRepositories>
</project>

これでHudsonのMavenプロジェクトでのCIも可能になりますね!

2009年5月3日日曜日

GAE/Jの単体テスト

基本はGoogleCodeで提供されているドキュメント、「Java/How-To/Unit Testing」の通りでおk。

単体テストに必要なモジュール

以下のモジュールがテスト実行環境に必要。

  • appengine-local-runtime
  • appengine-api-stubs.jar

Eclipse Plug-inを導入した場合は、"${ECLIPSE_HOME}/plugins/com.google.appengine.eclipse.sdkbundle_1.2.0.v200904062334/appengine-java-sdk-1.2.0/lib/impl"のようなフォルダに配置されている。バージョン番号を含んだフォルダ名なので、後々変わるかもしれない。mavenのリポジトリでは"http://www.mvnsearch.org/maven2/com/google/appengine/"配下のフォルダにあるのでここから拾えば良い。

テスト実行環境用のAPIプロキシの実装クラスを準備する

package com.shin1ogawa;

import com.google.apphosting.api.ApiProxy;

public class TestEnvironment implements ApiProxy.Environment {
  public String getAppId() { return "Unit Tests"; }
  public String getVersionId() { return "1.0"; }
  public void setDefaultNamespace(String s) {}
  public String getRequestNamespace() { return null; }
  public String getDefaultNamespace() { return null; }
  public String getAuthDomain() { return null; }
  public boolean isLoggedIn() { return true; }
  public String getEmail() { return null; }
  public boolean isAdmin() { return false; }
}

com.google.appengine.api.users.UserServiceFactory#getUserService()からcom.google.appengine.api.users.Userオブジェクトを取得してテストに使用するのであれば、isLoggedIn()でtrueを返し、getAuthDomain()とgetEmail()でnull以外を返してやれば良い。

テストクラスを作成する - その1

まずは@Beforeと@Afterだけ。

package com.shin1ogawa.service;

import static org.junit.Assert.assertEquals;

import java.io.File;
import java.util.List;

import org.junit.After;
import org.junit.Before;
import org.junit.Test;

import com.google.appengine.api.datastore.dev.LocalDatastoreService;
import com.google.appengine.api.users.User;
import com.google.appengine.api.users.UserServiceFactory;
import com.google.appengine.tools.development.ApiProxyLocalImpl;
import com.google.apphosting.api.ApiProxy;
import com.shin1ogawa.TestEnvironment;
import com.shin1ogawa.entity.Board;

public class BoardServiceTest {
  @Before
  public void setUp() {
    ApiProxy.setEnvironmentForCurrentThread(new TestEnvironment());
    ApiProxy.setDelegate(new ApiProxyLocalImpl(new File("target/testDataStore")) {
    });
    ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate();
    proxy.setProperty(LocalDatastoreService.NO_STORAGE_PROPERTY,
        Boolean.TRUE.toString());
  }

  @After
  public void tearDown() {
    ApiProxyLocalImpl proxy = (ApiProxyLocalImpl) ApiProxy.getDelegate();
    LocalDatastoreService datastoreService = (LocalDatastoreService) proxy
        .getService("datastore_v3");
    datastoreService.clearProfiles();
    ApiProxy.setDelegate(null);
    ApiProxy.setEnvironmentForCurrentThread(null);
  }
}

"new ApiProxyLocalImpl(new File("target/testDataStore")) {}"としているが、ここで指定したフォルダ配下にローカルストレージ用のファイルと使用したインデックスが配置される。ただし、今回はApiProxyへの設定で"LocalDatastoreService.NO_STORAGE_PROPERTY"を"true"に設定しているので、実際のファイルへの保存は行われない。あらかじめテストデータが格納された状態でのテストを行いたい場合は、この設定を"false"にした状態でデータの書き込みを行い、それをテストケース毎に保存しておくと良い。

テストクラスを作成する - その2

次に実際のテストクラス。今回は"Board"というEntityクラスと、それに関するアクセサを提供する"BoardService"というクラスがある事を想定して、そのServiceクラスのテストケースとして記述している。こんなカンジで、実際のテストメソッド内では特に特殊な事は何も必要無い。

  @Test
  public void create01() {
    User user = UserServiceFactory.getUserService().getCurrentUser();
    BoardService test = new BoardService();
    Board board = test.create(user, "test title1");
    List<Board> list = test.list();
    assertEquals(1, list.size());
    assertEquals("test title1", board.getTitle());
  }

  @Test
  public void delete01() {
    User user = UserServiceFactory.getUserService().getCurrentUser();
    BoardService test = new BoardService();
    Board board1 = test.create(user, "test title1");
    Board board2 = test.create(user, "test title2");
    List<Board> list = test.list();
    assertEquals(2, list.size());
    assertEquals("test title1", board1.getTitle());
    assertEquals("test title2", board2.getTitle());
    test.delete(board1);
    list = test.list();
    assertEquals(1, list.size());
    assertEquals("test title2", list.get(0).getTitle());
  }

注意点

上記の内容については「Googleが提供しているEclipseのPlug-inでプロジェクトを作成し、自分に必要なライブラリをEclipse的にAdd Build pathした環境」で動作するためのもの。MavenとEclipseを組み合わせているとはまる可能性大。自分はそれではまった。MavenからEclipseプロジェクトを作成していると、datanucleus-core内のモジュールがCLASSPATHに重複しているとなんだかエラーが出てしまい、JDOのモデルクラスのエンハンスが正しく行われなかったり、まぁ色々あったりする。

ただし、Mavenからの単体テストの実行もうまくできる方法がわかっているので、それも後で書く。