2009年6月12日金曜日

Google Developer Day/Hackathon編

自分は「MailサービスとURLFetchサービスの単体テスト方法を探る」というカンジの、アプリを作るという事とははずれた事をテーマに作業した。なんとか発表には間に合わせたが、チームじゃなくて個人で作業した方も多かった事で発表者が非常に多くなり、あんまり時間をかけては迷惑だろう(発表順が二人目だった&人数から逆算すると3分くらいか?という計算)、という焦りと外部ネットワークに繋がらなかったという影響のため、実際に動作を見せる事ができなかった、という残念な結果に。その分このエントリで細かく書いておこう、と。ついでに、ハッカソン終了後の改良も追加しておくw

ちなみに、問題無く実行されると以下のように出力される予定でした。originalがプロダクトコードからの実際のリクエストやレスポンスで、modifiedがすげ替えたものの値…。

Mailサービス

Mailは簡単。ドキュメントもあるくらい。

URLFetchサービス

こいつが大変だった。ドキュメントを見ると、単体テスト時はダイレクトに実際のURLに接続します、みたいに書いてある。というのも、URLFetchに関しては通常のjava.net.HttpURLConnectionがを使うため、SDKが起動していない状態だとHttpURLConnection(sun.net.www.protocol.http.HttpURLConnectionとか)が適用されてしまい、ふっつーに指定されたURLにアクセスしてしまうため。

極力プロダクトコード側には仕組みを埋め込みたくない、となるとHttpURLConnectionを自前で実装してそれを適用させるのが近道じゃね?というのが最初の案。この方法なら低レベルでの話だしGAEも全然関係ないだろし、と。書いていくと、すぐに"URLStreamHandler"クラスにエラーマーカーが。

とはいえ所詮Eclipse上のGoogleプラグインのValidateだけの話でJava的にはエラーではないし、Testコードのため実行時には問題は起きないし、まぁいいか?無視しちゃう?とは思ったが、Eclipseを使う事が多い人はずっとエラーマーカーが残ったままってのは気持ち悪いはずなので別の方法を考えてみる(警告のマーカーだったら無視してたかもw)。

んじゃ手を抜くのはあきらめて、たぶんAppEngineの試験という意味では王道であろう、ApiProxyのDelegateをいじるとするか、と。GDDのGAEJセッションでもそんな話が出てたし。

URLFetchでもApiProxyを経由させたい

ApiProxyを経由させるためには、SDK上で実行しているときと同じHttpURLConnectionの実装を適用したい訳で、これを設定しているメソッドを探す。デバッグしていくと、com.google.appengine.tools.development.StreamHandlerFactory.install();の中でURL.setURLStreamHandlerFactory()している事がわかるので、これをTestコード実行時の早い段階で実行してみると、無事にApiProxyを経由している事が確認できた。

ApiProxyLocalを実装

公式ドキュメントの単体テストの資料にあるソースでは

ApiProxy.setDelegate(new ApiProxyLocalImpl(new File(".")){});
とか書かれていたりする、ApiProxy.setDelegate()で設定するモジュール。こいつを実装してやるわけです。その実装の中でドキュメントにあるApiProxyLocalImplのインスタンスを保持し、こいつに各処理を委譲してやります。もちろん、全ての処理を委譲したら何の意味も無いので、一部のメソッドは委譲したりしなかったりするわけで、そのメソッドはbyte[] makeSyncCall(Environment, String, String, byte[])です。一見getService()だな、と思うけど、それは実はどーでも良い(実装の方法に依存するけれども、makeSyncCall->getService()という実行順なので)。

実行時には、まずサービス名とサービスクラスのメソッド名、メソッドの引数がmakeSyncCall()に渡されて、makeSyncCall()getService(サービス名)で実行すべきサービスを取得して実行すべきメソッドをディスパッチする、といったカンジ。引数や返り値はbyte[]配列で統一されているが、ProtocolBufferかなー、と。これらを考慮して、以下のように実装してみた。

コメントにある通り、とりあえずURLFetchServiceだけに対応している。が、 serviceMocksというMapを使ってサービス名をキーにLocalRpcServiceの実装を保持するようにしている。

で、TestCaseの抽象クラスは以下のようなカンジに。

そしてURLFetchService用にインターフェースと抽象クラスを用意した。

実際にこれを使用しているテストケースは以下のようなカンジ。テスト対象はここには書かないけど、FriendFeedのAPIを使用するクラスとなっている。ユーザ情報を渡さずにpublic timelineを取得したり、nicknameとremoteKeyでBasic認証を行ってユーザのHome Feedを取得したり。

UrlFetchServiceMock.AbstractUrlFetchServiceMockをインスタンス化し、抽象クラスから取得したApiProxyLocalMockaddService()をしてます。で、UrlFetchServiceMock.AbstractUrlFetchServiceMockではabstractになっていたURLFetchResponse fetch(URLFetchRequest)、つまり実際にレスポンスデータを作成するメソッドを実装しています。ここではURLやリクエストヘッダを見て返すデータ(テストデータとレスポンスコード)を振り分けています。

datastoreやmailサービスと違い、結構面倒臭いです。面倒くさいですが、テスト時に外に出て行って想定できないデータをとってきたりするのも困るワケで、データを固定に出来る(エラーも試しやすい)とでテストケースも書きやすくなるんじゃないかと。また、作業したソースは以下のSVNRepositoryに存在しています。

余談(SDKのソースについて)

実は作業を始める前に、SDKのソースが無いと不便だと思って、日本のGooglerに「Java版のSDKのソースが見つからないのですが、公開されていますか?」と質問してみた。すると「無いといぅ事は無いんじゃないかなー」と探してくださった。が、結論は「無いっぽいね」となった。それはそれで「んじゃ面倒だけどjadして確認します」とか言って作業に入ったんだけど、その後twitterでid:しげるんば(なぜかリンクされry)が「リバース禁止条項があるかも」と知らせてくれた。たしかに、そう書いてある…。特に改変配布もせず覗くだけなら良いだろうけど、ハッカソンの発表には使えないんじゃね?と思って、実はEasyMock等の使い慣れたやつで無理矢理片付けよぅとも考えていたり。が、松尾さんに相談したところそのまま海外のGooglerに持ち込んでくれて、「ま、改変して配布するとかがなく、こっそり覗くんならいーーんじゃね?」くらいのお話を効かせていただけた。おかげで調査も進んで、本来の姿かな?と思う方法に落ち着かせる事ができた。海外のGooglerの方も「公開されていると思うよ?」とおっしゃってたので、実は公開する気があるけど忘れてるだけ?とかも思う。それであると嬉しい。

また、今回書いた方法よりもいい方法があるんだろぅと思うし、どんくさい方法だな、と思ったらもっと良い方法を是非教えてください!

API Expertの方、Googler様、一緒に開発した方々、ありがとーございました。

コメントを投稿