2013-11-18

第4回 #渋谷java で JUnit のオブジェクト等価比較処理を怠ける (ために Matcher を頑張る) トークをしてきました

通算3回目の参加・発表ということで大変お世話になっている 第4回 #渋谷java でお話してきました。5 分の LT では語れなかった内容が多々あるので、それらを盛り込んでエントリを残しておきます。

発表資料は以下です。

発表で使用する予定だったデモコードは komiya-atsushi/shibuya-java4 に置いておきました。

オブジェクト等価比較を怠けたくなった背景

事の発端

元々は SAStruts なとあるお仕事において、JSON を返却する Web API の Action クラスの各メソッドを JUnit でテストしたい、というのが事の発端でした。

このテストの本来の理想的な姿は、Tomcat などの AP サーバを経由して HTTP で実際に Web API を呼び出し、その結果のレスポンス の JSON が期待している内容と同一かどうかを比較する… というものでした。しかし、この構成を JUnit / S2JUnit4 で実施するにはなかなか難儀することが予想された(ぶっちゃけ面倒だった)ため、早々に Action クラスのメソッドを直接呼び出すことで得られる JSON 文字列を (デシリアライズして) アサーションする、という現実的な方針に切り替えました。

そういうわけで、Web API から返されるちょっと複雑なデータの JSON を Java のオブジェクトとして取り扱いつつ、それに対して期待している結果と等しいかどうかの等価比較 ( assertThat(actual, is(expected)); ) をすることが求められたわけですが… まあ Web API の戻り値として返すようなオブジェクトのクラスに Object#equals() がオーバーライド実装されていることなんてそうあるわけでもなく、ただひたすらにオブジェクトにプロパティ経由で値を参照して assertThat(actual.getHoge(), is(expected.getHoge()); する、みたいなテストコードの記述を強いられる残念な状況が生じたわけです(さすがに、さしみたんぽぽ作業をただひたすらに続けられるほど、僕はできているエンジニアではなかったので、2, 3 行試しに書いてみたところで見事に崩折れました)。

Object#equals() の実装

assertThat() の羅列が本当に羅列すぎて生きるのが辛いので、本来は equals() の実装は不要だけれども JUnit でのテストのために致し方なく本末転倒的にオーバーライドすることも念の為に考えてみました。

幸いにして、 Apache commons-lang の EqualsBuilder を使うと、比較的容易に equals() を実装することができます (commons-lang については こちらの資料 が参考になります)。特に、 EqualsBuilder#reflectionEquals() を使うと驚くほど簡単に equals() を実現できます。

しかし一方で、 EqualsBuilder を使って equals() を実装してアサーションを行うようにすると、アサーションに fail したときのメッセージが以下のように役立たずなメッセージになる、というデメリットが存在します。

java.lang.AssertionError: 
Expected: is <foo.bar.HogeBean@9a8d9b>
     but: was <foo.bar.HogeBean@22c6bb6c>

Matcher による解決の試み

Object#equals() を実装していないクラスのオブジェクトに対して等価比較なアサーションをしたい場合、 org.hamcrest.BaseMatcher クラスを継承実装するという手があります。この方法を利用すると、

  • 等価比較だけでなく、値の大小比較などのきめ細かいアサーションが可能になる。
  • fail したときの mismatch メッセージに不一致となった内容や各種コンテキスト情報などを載せることができ、fail メッセージからの不具合の原因追求が容易になる。
  • assertThat() まわりの記述がすっきり・シンプルになる。

というメリットを得ることができます。ただ、Web API のようにエンドポイントごとに返却されるオブジェクトが異なる場合などはエンドポイントの数だけクラスが実装されていることもあり、そのクラスの数だけ BaseMatcher クラスを継承実装するというのは現実的ではありません (そもそもの BaseMatcher の実装コストがかかる、というデメリットもあります)。

汎用的な等価比較用 Matcher を目指す

そういうわけで、「equals() を実装する」わけでもなく「個別 に Matcher を用意する」わけでもない方法で解決するアプローチとして、汎用的に利用できる等価比較用の Matcher である IsEquivalentTo を作成しました。

この Matcher を利用することで、

  • equals() の実装を必要とせず
  • もちろん独自 Matcher も実装不要で
  • assertThat() まわりのコードも比較的シンプルに保ち
  • 等価比較したくないプロパティ / フィールドも明示できる

テストコードの記述が可能になります! (なる予定です!)

Matcher の汎用化、というわけで、個別に Matcher を実装したときと比べてきめ細かなアサーションは実現が難しくなりますが、等価比較できるところだけをこの汎用化 Matcher を利用して比較し、等価比較ではないアサーションは別途 assertThat() の羅列などで対処する、というアプローチもとれることでしょう。

技術的だったり細かい話

IsEquivalentTo の実装では、以下の工夫を盛り込みつつ、等価比較の汎用化を実現しています。

  • 再帰的なオブジェクトのトラバース ... オブジェクトを木構造なデータ構造と見立て、List / Map / 配列 / その他オブジェクト、の 4 種類ごとに等価比較の戦略を分けています。List / Map / 配列であれば、単純に保有しているオブジェクトに対して等価比較をするようにします。

  • オブジェクトの等価比較 ... Object#equals() をオーバーライド実装していないオブジェクトの場合、単純に equals() による等価比較をしてしまっては残念な結果になることは目に見えているので、以降に示す「プロパティの参照」「public フィールドの参照」を試すようにしています。一方で equals() をオーバーライド実装しているようであれば、その equals() を呼び出すようにします。

  • プロパティの参照 ... JavaBeans の規約に従って定義されているプロパティは、JavaBeans API の java.beans.PropertyDescriptor を利用して、getter メソッド経由で値を読み出すようにしています。

  • public フィールドの参照 ... 最近だと、JavaBeans の規約にあえて従わず、public にフィールドを定義してクラスの外にフィールドをさらけ出すような実装を見かけることも、実際にそのような実装をすることも多くなってきました (MessagePack が要求するクラスがまさにそのようなフィールド構成ですね)。そのため、public なフィールドについてはリフレクション API を利用して、プロパティの場合と同じようにその内部の値を取得・参照するようにしています。

  • 例外的に扱いしたいプロパティ / フィールドなどの指定 ... テスト実行した日時の情報など、不定な値がオブジェクトに含まれる場合は、その値を無視して等価比較したくなるものです。そのようなケースを考慮して、 JSONPath 的な記法で等価比較したくない要素を指定できるようにしています。

  • 比較的親切な fail 時の mismatch メッセージ ... JUnit のテストケースを作成する上で重要なことの一つに、テストケースが fail したときに「何が原因で fail したのか?」ができる限り把握できるメッセージを出力することがあります。 IsEquivalentTo の実装では、オブジェクト階層を意識して、JSONPath 的な表現で、「オブジェクトのどこがおかしいのか?」が把握可能なメッセージ出力ができます。

会場参加者への逆質問

今回話題にした等価比較の問題、他の人はどういうアプローチをとっているのかなー? と気になったので、会場参加されている方々に 逆質問してみたところ、

  • equals() が実装されていることは確かに少ないけど、 toString() が実装されていることは結構あるので、文字列比較で済ませている。
  • 割り切ってテストはしない。

との回答をいただきました。不定な値がオブジェクトに紛れることがなければ、確かに toString() で比較できますね… 参考になります。

その他、感想などなど

  • build.xml と JSP はそろそろ滅んで欲しい!
  • Groovy / Grails に俄然興味が湧いてきた!
  • TokyoWebmining の新たな講師がゲットできそう!
  • 資格試験 (統計検定) の前日に発表予定を入れるのはもうやめよう…

最後に、会場提供の BizReach さん、そして主催の @seri_k さん、お疲れさまでした&ありがとうございました!!

9:21No comments

2013-09-03

第 4 回 #DSIRNLP データ構造と情報検索と言語処理勉強会でお話してきました。

3週間ぐらい前かつ発表前後は業務的に憤死しててもおかしくない時期ではありましたが、ありがたいことに主催者の @overlast さんから今回の発表オファーを頂いたしせっかくだから何か話そう、と心に決めて、いまの会社 でのお仕事でよく取り扱っているコサイン類似度や Jaccard 係数などのいわゆる「類似度計算」の時間/空間計算量的に効率的な処理を実現する方法についてお話してきました。

自分含めた発表内容の一覧は こちら を参照してください。

発表スライドはこちらです。

発表後に頂いた質問はこんな感じだったはず。寝不足ゆえの言語認識力がだいぶ低下した状態で受け答えしたので、質問/回答は超意訳になってます。悪しからず。

  • バスケット分析的な方法の計算量的な話

    • 購買ログなどからバスケット分析的な方法で類似度計算しようとすると、ユーザごとにログを集約しなければならないという追加処理が必要となるものの、計算量的にはあんまり問題になってない。
    • むしろ問題になるのはアイテムの数。アイテム数の2乗に応じて空間計算量が必要になってしまうため、アイテム数が多い場合にはこの方法は向かない。
  • レコメンド精度の面で、Jaccard 係数とかコサイン類似度ってどうなのよ?

    • ビジネス的には、類似度計算によって得られる精度よりもドメイン知識を使ったフィルタリングを良くする方が大事だと考えてる。
    • あと弊社の事情となってしまうが、精度のよい弊社独自の類似度計算のロジックは社外秘的な扱いとなっているためしゃべれない(ので、あえて精度の話は触れなかった)。

今回の発表にあたって b-bit Min-wise hashing を Java で実装してみましたが、精度・処理性能面の特性がまだ把握しきれていないので、もうちょっと追求してみたいですね。現状の実装は以下のとおりです。

https://gist.github.com/komiya-atsushi/6414548

あと、他の方の発表の感想と今回の反省です。

  • ダブル配列の実装テクニック的な話が熱かった。Java で trie を実装するときの参考になるのでとってもためになります。

  • 浮動小数点型数値の圧縮の話、昔のとあるお仕事で FPC を組み込もうかという検討をしたことがあって、そのときのことを思い出してちょっと感慨深いものがあった。整数圧縮の部分を改善する余地がまだあるとのことなので、maropu 先生の次回作に超期待してます!

  • 僕の発表、頂いた時間内でちゃんと発表しきれたのはよかったが、聴講者の反応を見てるとあまり興味ない or 説明が不足しているゆえに理解が追いつかない状態だったっぽい。魅力的な発表テーマ&必要十分な説明を心がけたいですね。

  • 忙しいときこそ没頭してしまう「艦これ」を断つ勇気を僕にください…

10:18No comments

2013-08-21

Play! 2.x で JUnit が実行できなくて生きるのが辛い

いままでは IntelliJ IDEA 上で JUnit によるユニットテストを実行していたのですが、Play! のバージョンを 2.1.1 から 2.1.3 にマイグレーションした際に IntelliJ 上での JUnit 実行ができなくなってしまい、解決法を探るのが面倒そうだったので play test で暫定対処しようとしたものの、こちらもこちらでいろいろと地雷が埋まっていたので解消・回避方法をメモしておくことにします。

その 1 : 'play clean' してから 'play test' する

とりあえず play test してその結果が以下の表示になる場合は、 play clean をしましょう。

[warn] 1 warning
[info] No tests to run for test:test 

ここ に書かれているとおり、

You might try doing a play clean and then try play test again. I was getting the same "No tests to run" message until I did a clean.

ということで、play clean する必要があるそうです。play のコンソールを立ち上げた状態で clean タスクを実行しても思ったようにクリーンされないようなので、いったん play コンソールを抜けてから play clean するとよいです。

その 2 : junit-interface で work around する

上記の play clean をしてから play test をしても、(テストクラスは認識されるものの) それぞれのテストケースの実行結果が

[info] 0 tests, 0 failures, 0 errors

などと、個々のテストメソッドが実行されないことがあります (というかほぼ確実に実行されない?)。 そのような場合は、こちら に書かれているとおり、Build.scala に junit-interface への依存関係を追加すると問題を回避できます。

以下は Build.scala への追記例です。

  val appDependencies = Seq(
    // Add your project dependencies here,
    javaCore,
    javaJdbc,
    javaEbean,
    "play" %% "play-test" % play.core.PlayVersion.current % "test" exclude("com.novocode", "junit-interface"),
    "com.novocode" % "junit-interface" % "0.9" % "test"
  )

これでやっと play test でユニットテストが実行できるはず!

13:231 comment

2013-07-28

Play 2.x で sbt.ResolveException が発生する問題への対処

Play 2.x で開発していたアプリケーションをステージング環境で play run したり、プロダクション環境で稼動させるために play dist したときに、以下のように、sbt.ResolveException の例外が発生した旨のログ出力がされて異常終了してしまったときの対処法をメモっておきます。

[warn] some of the dependencies were not recompiled properly, so classloader is not avaialable
[info] Updating {file:/path/to/play-app/}play-app...
[warn]  [NOT FOUND  ] org.slf4j#slf4j-api;1.6.6!slf4j-api.jar (1ms)                    
[warn] ==== local: tried
[warn]   /opt/playframework/repository/local/org.slf4j/slf4j-api/1.6.6/jars/slf4j-api.jar
[warn]  ::::::::::::::::::::::::::::::::::::::::::::::
[warn]  ::              FAILED DOWNLOADS            ::
[warn]  :: ^ see resolution messages for details  ^ ::
[warn]  ::::::::::::::::::::::::::::::::::::::::::::::
[warn]  :: org.slf4j#slf4j-api;1.6.6!slf4j-api.jar
[warn]  ::::::::::::::::::::::::::::::::::::::::::::::
sbt.ResolveException: download failed: org.slf4j#slf4j-api;1.6.6!slf4j-api.jar
        at sbt.IvyActions$.sbt$IvyActions$$resolve(IvyActions.scala:214)
            (略)
        at java.lang.Thread.run(Thread.java:724)
[error] (*:update) sbt.ResolveException: download failed: org.slf4j#slf4j-api;1.6.6!slf4j-api.jar

原因

Play プロジェクトを構築した時の Play framework のバージョンと、play run などをしようとしたときのバージョンが異なる場合にこの例外が発生するようです。

より具体的には、Play プロジェクトが利用する sbt-plugin のバージョンは、実行環境の Play のバージョンと合わせる必要がある、ということです。

実は play run を実行したときに、実行直後のログに赤文字で

This project uses Play 2.1.1!
Update the Play sbt-plugin version to 2.1.2 (usually in project/plugins.sbt)

と出力されていたりします。どうしても例外ログの方に真っ先に目が行ってしまいがちですが、ちゃんと Play framework は警告してくれています。

対処法

project/plugins.sbt に記載されている sbt-plugin のバージョンを、実行環境の Play のバージョンと合わせましょう。例えば、Play プロジェクトを作成したときの Play のバージョンが 2.1.1 であれば、

addSbtPlugin("play" % "sbt-plugin" % "2.1.1")

となっているかと思いますが、これを、Play 2.1.2 の環境で稼動させようとするなら

addSbtPlugin("play" % "sbt-plugin" % "2.1.2")

としましょう。

19:54No comments

2013-06-29

第1回 #渋谷Java でトークしてきました

渋谷java という、渋谷界隈で Java やってる人達が集って LT するイベントが開催される、ということで (僕が勤めている仕事先は拠点が代々木ではありますが) 参加&お話をしてきました。

話した内容

元々は自社内で開催している「エンジニアリング勉強会」で展開した、Java アプリケーションを開発する際にパフォーマンス上注意すべき Tips を、外部向けに再構成してお話しました。ATND には「初心者向けレベルだとまだjavaを触って日が浅い主催者が泣いて喜びます。」と書いてあったので、初心者がやってしまいがちな、パフォーマンスを悪化させてしまうコーディングを中心にケーススタディとして挙げています (実際の参加者はガチ勢多数という感じでしたけどね!)。

再構成前の資料は5つぐらいのケーススタディを用いて 1 時間の説明をしていたのですが、今回は LT ということでページをバッサリと削って発表に臨みました。それでも、予定時間の 10 分より +6 分ぐらいオーバー、ということで時間内でしゃべりきるというのはやっぱり難しいなあ、と反省する次第です。

感想

以下、他の方のトークも含めた感想です。

  • Java の言語コミュニティなのに、ジャバ = エンタープライズ臭漂う残念めな言語、という認識の参加者が多いこと。
  • 仕事で Java 使っているからといっても Java が好き、というわけではない。Ruby が好きとか、仕方なく使っているとかそういう人もいる。
  • Play framework の認知度が高まってきている印象。Java で Play 2.x は微妙的な意見もあった (し、それを否定する気もさらさらない) けど、Java の世界でもっと流行ってくれるといいなー。
  • BizReach 社のセミナールーム (?)、オシャレですね!
  • 渋谷Java、今後も継続開催していただけることを期待してまーす!
18:12No comments

2013-06-19

Play! 2.0 / 2.1 ぐらいで play コマンドを実行したときに Permission denied な例外が発生する問題への対処

イラッとしたのでメモ。

概要

Linux など UNIX 系 OS に Play framework 本体をインストールし、${PLAY_HOME}/play コマンドで play clean compile stage などをした場合に、

java.io.FileNotFoundException: /path/to/play/framework/sbt/boot/sbt.boot.lock (Permission denied)
        at java.io.FileOutputStream.open(Native Method)

とか

java.io.FileNotFoundException: /path/to/play/repository/.sbt.ivy.lock (許可がありません)
        at java.io.FileOutputStream.open(Native Method)

などの Java の IOException が発生してしまうことがある件への暫定対処です。

原因

そもそも何でこの例外が発生するのかというと、Play framework 本体をインストールした際のユーザ (所有者) であったりパーミッションの設定が厳しかったりすることが原因となり、上記した類のロックファイルの作成や書き込みができないことによります。

※ play コマンドを実行したときに、${PLAY_HOME} ディレクトリ以下に書き込みをする Play framework もどうかと思いますがね…

(暫定) 対処法

本当はもうちょっとスマートな方法があるんじゃないか… と思ったのですが何かと面倒そうなので、

chmod -R go+wx ${PLAY_HOME}/framework/sbt/boot
chmod -R go+wx ${PLAY_HOME}/repository

こんな感じで解決するのがいいでしょう。

2013.8.21 追記

よくよく Play! 2.x の公式ドキュメント http://www.playframework.com/documentation/2.1.3/Installing を読んでみると、

Download the latest Play binary package (take the latest official version) and extract the archive to a location where you have both read and write access. (Running play writes some files to directories within the archive, so don’t install to /opt, /usr/local or anywhere else you’d need special permission to write to.)
と書かれていました。つまり「(ファイル/ディレクトリ書き込みなどをするから) /opt, /usr/local にはインストールするなボケ!」ということですね。

23:371 comment