Android Orma v2.4.0でtype adapterを一つのクラスに集約できるようにした

Orma v2.4.0をリリースしました。差分は大きいですが、アノテーションプロセッサのリファクタがほとんどです。

Ormaのtype adapterは static type adapters というやつで、コンパイル時にtype adapterの検索と呼び出しの埋め込みを行うので動的に処理するものとくらべて高速でしかもコンパイル時にエラーを検出できるという特徴があります。

これについては以前少し説明したことがありました。

Android ORMでオブジェクトの埋め込みはどのように実装するか - Islands in the byte stream

Orma v2.4.0 では、このtype adapterを一つのクラスに集約できるようになりました。どのみちstatic methodしか使わないのですし、集約するほうが見通しがいいと思います。

構文としては、本当はJava8のrepeating annotationsができればいいのですが、それはできなかったので同じことをJava8以前の構文でやる方法をとりました。 MutableIntMutableLong のtype adapterを一つのクラスにまとめるとこんな感じです。補完をつかって書けないのがつらいところですが、それはstatic methodを使う以上は仕方ないですね。

@StaticTypeAdapters({
    @StaticTypeAdapter(
        targetType = MutableInt.class,
        serializedType = int.class,
        serializer = "serializeMutableInt",
        deserializer = "deserializeMutableInt"
    ),
    @StaticTypeAdapter(
        targetType = MutableLong.class,
        serializedType = long.class,
        serializer = "serializeMutableLong",
        deserializer = "deserializeMutableLong"
    )
})
public class TypeAdapters {

    public static int serializeMutableInt(@NonNull MutableInt target) {
        return target.value;
    }

    @NonNull
    public static MutableInt deserializeMutableInt(int deserialized) {
        return new MutableInt(deserialized);
    }

    public static long serializeMutableLong(@NonNull MutableLong target) {
        return target.value;
    }

    @NonNull
    public static MutableLong deserializeMutableLong(long deserialized) {
        return new MutableLong(deserialized);
    }
}

Gsonの処理を爆速にするStaticGsonをリリースした

コード生成でGsonをMoshiより高速化する - Islands in the byte stream の続きです。

GitHub - gfx/StaticGson: Static Gson binding library with annotation processing

三行まとめ

  • StaticGsonはannotation processingでコード生成してGsonを高速化する拡張で、結果はLoganSquareより少し遅い程度
  • 欠点はメソッド数+バイナリサイズのオーバーヘッド
  • 利点はGsonと互換性の高さ。モデルにアノテーションをつけてGson初期化でオプションを一つ与えるだけ

解説

リフレクションで処理していたところをコード生成にして高速化する手法は思ったより効果ありそうだな、ということでjcenterにリリースしました。以下のように依存指定すると使えます。

dependencies {
    apt 'com.github.gfx.static_gson:static-gson-processor:0.9.4'
    compile 'com.github.gfx.static_gson:static-gson:0.9.4'
}

使い方は簡単で、シリアライズ対象のモデルに @JsonSerializable をつけて:

@JsonSerializable
public class Book {
  // ...
}

GsonBuilderにtype adapter factoryを与えるだけ:

Gson gson = new GsonBuilder()
        .registerTypeAdapterFactory(StaticGsonTypeAdapterFactory.newInstance())
        .create();

setDateFormat()などのGsonBuilderのoptionはだいたい効きますが、FieldNamingPolicyなど一部のリフレクションの絡む機能は別途@JsonSerializableに与える必要があります。READMEにもあるとおりこんな感じ。

このシリアライズ名はコンパイル時に決定されるので、proguard避けのために@SerializedName を設定する必要はありません。速度よりもこっちのほうが個人的には嬉しい。

// LOWER_CASE_WITH_UNDERSCORES: 名前は各パーツをlower caseにしてアンダースコアで繋げるものとする
@JsonSerializable(
  fieldNamingPolicy = FieldNamingPolicy.LOWER_CASE_WITH_UNDERSCORES
)
public class User {
    public String firstName; // serialized to "first_name"
    public Stirng lastName; // serialized to "last_name"
}

benchmarkはLoganSquare のものに手を加えてStaticGson対応させたもので測定してこんな感じ。Web APIのパースが多いだろうからパースに関する結果を載せています。Gsonよりも30%くらいは高速で、Jacksonより速くてLoganSquareより遅いという水準ですね。すでにGsonを使っているアプリに手軽に適用できるし、Gsonのオプションもほぼそのまま有効なので、導入が低コストという意味ではいいかもしれません。新規で使うならLoganSquareのほうが高速ではありますが、様々な場合にちゃんと対応できるかどうかまでは調べてないのでそこはよくわかりません。

I/O MultiplexingでAndroidのための効率のよい画像ローダをつくる検証

モバイルアプリのスレッドプールサイズの最適化(画像読み込み編) - クックパッド開発者ブログ

これに対する「I/O多重化すればスレッド数とか気にしなくていいんじゃないの」という意見を聞いて、それもそうだなと思ったので検証してみました。

前提知識

  • IO多重化にはjava.nio.channels.Selectorjava.nio.channels.SocketChannel を使う
  • Selector は Perlの IO::Select によく似たインターフェイスと機能を持つ
    • つまり select(2) ないし同等のシステムコールへのインターフェイス
    • Androidの場合に使われるのは epool(2) かも
  • SocketChannelSelector で多重化できるsocket
  • Androidのdocumentはあまりないが、Oracleのドキュメントがあるので参考になる
  • Oracle JDK7にはNIO.2というものがあってより多重化をやりやすいらしいけど、Androidにはないので関係ない

結果

とりあえず最低限HTTP 1.0で複数の画像のURLをリクエストしてレスポンスをパースしてviewに表示させるところまではできました。response headerのパースを真面目にやってないので、このままプロダクションで使うのは無理ですが、とりあえず検証はできるのでこれでよしとします。サポートしているプロトコルは HTTP/1.0 だけで、HTTPSは扱えません。

実機*1で検証したところと、シリアルにリクエストを送る場合は1000msくらいかかるところ、IO多重化したほうは300msくらいで済みます。この実装だと1つのスレッドを専有するうえにプライオリティも考慮しませんが、実験的なコードとしては十分でしょう。

というわけで、AndroidでもIO多重化は有効だとわかりました。最強の画像ローダを実装する場合は、画像のロードはIO多重化してシングルスレッドで行い、デコードはCPUの数だけスレッドを作って行うのが理想的だと思われます。

*1:Xperia Z4 / Android 5.0.2 / LTE

「Android NでJava8がサポートされた」とはどういうことか

追記: StreamやOptionalはpreview-2で実装されたようです。

gfx.hatenablog.com


Android N previewが公開されましたね!このバージョンではJava8のサポートがあると発表されています。また、標準クラスライブラリがOpenJDKベースの実装になったことで、Java8との互換性が高まるのではないかという前評判もありました。

First Preview of Android N: Developer APIs & Tools | Android Developers Blog

本エントリでは、この「Android NでJava8」について解説します。

三行まとめ

  • Android N runtimeはOpenJDK7ベースで、Java8クラスライブラリは一部のみ移植されている
  • Android N SDKに同梱されているJackコンパイラはlambdaなどの一部のJava8の構文を古いAndroid向けのdexにコンパイルできる
  • JackコンパイラができることはRetrolambdaとほぼおなじで、Android Nと直接は関係ない

解説

さて、「Android Nで」というのは、まず2つの意味があります。

  • Android Nの世代のAndroid SDKで開発するとき
    • compileSdkVersion=N かつ targetSdkVersion=N を要求することもあるものの、基本的には動作するAndroidのバージョンにはそれほど依存しない
  • Androidアプリケーションを動かすOSがAndroid N以上のとき
    • つまりNランタイムのうえで動作するとき

この2つを混同してはいけません。この記事では前者を「N SDK」、後者を「N runtime」と呼ぶことにします。

またさらに、公式サイトによれば、一部のJava8の機能は新しいJava Android Compiler Kit "Jack" でのみ提供されるとあります。これは、フルスクラッチで開発された*1Android専用のJavaコンパイラで、直接dexファイルを出力できます。ただし、Jackはまだannotation processingをサポートしていないなどの制限があるため、実際のアプリケーションで採用するのは難しいでしょう。正規版までにannotation processingサポートが入るといいのですが。

ところで現在すでにRetrolambdaで一部のJava8構文が使えます。Retrolambdaを採用したプロジェクトはすでにいくつかありますし、NとRetrolambdaを比較検討したいところです。

よって、考慮すべきは「ビルドがN SDKかJackかRetrolambdaか」の3パターン、実行環境としては、古いAndroid OSの代表として「ICS」、そして「N」の2パターン、この2つの組み合わせで考える必要があります。

それぞれで、Java8の構文とクラスライブラリの幾つかをピックアップして使用可能かどうかを表にしてみました。

※ Nはpreview-1版であり、正規版では結果は異なります。minSdkVersion=14が効かない問題があり、動作はさせていません。下記の表は単なるビルドの成否などから推定している部分もあります。

N SDK x ICS runtime N SDK x N runtime Jack x ICS runtime Jack x N runtime Retrolambda x ICS runtime Retrolambda x N runtime
lambda expressions no no yes yes yes yes
default interface methods no no yes yes yes yes
static methods on interfaces no no yes yes yes yes
repeatable annotations no no yes yes yes yes
optional no no no no no no
java.util.function.* no yes* no yes no yes
java.util.stream.* no no no no no no
  • Java7モードでビルドしてlambda式などを使わずにjava.util.function.* を使用することだけならできます。

純正のN SDKだとほとんど何も使用できませんが、これはN SDKのdexコマンドがJava8用に出力されたクラスライブラリのバージョン(52)を処理できないためです。

なおいくつかのAOSPのクラスをOpenJDK7 / OpenJKD8と比較したところ*2、基本的にはOpenJDK7をベースとしており、 List#forEach() などの一部のメソッドのみOpenJDK8から移植しているようです。

以上を踏まえると、Android N preview-1で使えるJava8は「Java 7.5」ともいうべき中途半端な状態ですね。もちろん正規版が近づいたらまた状況は変わるかもしれないので、あくまでも「N preview-1では」ということですが。

そしてクラスライブラリ以外の新構文は、実はNとは関係ありません。First Preview of Android N: Developer APIs & Toolsのエントリでも、Jackで提供される言語機能はGingerbread (Android 2.3)でも使えると書いてあります。このあたりをまとめて「NでJava8をサポート」と発表するのは開発者に混乱を招くのでやめていただきたいですね。

関連エントリ

コード生成でGsonをMoshiより高速化する

高速化しました。ざっくり実装しただけなのでリリースはしていません それなりに有効そうなのでjcenterにリリースしました。*1

ベンチマークについて

まずスコアをみていただきましょう。dynamic gsonがGoogle Gson v2.6.2、static gsonが今回最適化したもの、moshiが Square Moshi v1.1.0 です。ベンチマーク環境はXperia Z4 / Android 5.0.2です。(2016/03/09 初版よりデータ構造を複雑にして結果を更新。最新データはStaticGsonのREADMEを参照してください。)

D/XXX     : start benchmarking Dynamic Gson
D/XXX     : Dynamic Gson in serialization: 449ms
D/XXX     : Dynamic Gson in deserialization: 387ms

D/XXX     : start benchmarking Static Gson
D/XXX     : Static Gson in serialization: 198ms
D/XXX     : Static Gson in deserialization: 233ms

D/XXX     : start benchmarking Moshi
D/XXX     : Moshi in serialization: 270ms
D/XXX     : Moshi in deserialization: 656ms

serializeもdeserializeもstatic gsonはdybamic gsonよりも高速ですし、またmoshiよりも高速です。というか、moshiのdeserializeはgsonより遅くてどうしたんだという感じですが。これは繰り返しとっても変わらないので、実際に高速化できていると思います。

ターゲットとなるクラスはなるべくリアルなものがいいので、 droidkaigi2016 から拝借して少し改変したSession.javaです。

StaticGson/Session.java at master · gfx/StaticGson · GitHub

ベンチマークコードはこちら。

StaticGson/MainActivity.java at master · gfx/StaticGson · GitHub

N = 3000 はXperia Z4でGCが起きないような値です。つまり上のスコアのときGCは起きていません。他の端末の場合、GCが起きないように調整してスコアを取らないと正確にはならないので注意してください。

なぜ高速なのか

Annotation ProcessingでモデルごとのJsonAdapterFactoryを生成しています。これは、Gsonがもともとリフレクションで行っていることを静的に行うようにしたものです。リフレクションは通常のフィールドアクセスと比較して非常に遅いので、リフレクションを使わないだけで該当箇所は数倍高速になります。ただし、文字列解析や文字列生成が高速になるわけではないので、現実的にはせいぜい数割スコアが上がる程度です。

また、コード生成の副作用としてProGuardで壊れることがなくなるというのもメリットです。ProGuardが壊すのはリフレクションを使ったコードだけです。

生成されるコードは次のようになります。これはまさに、GsonのストリーミングAPIを使って手書きでTypeAdapterを書くとこうなるだろう、というコードです。

// ... imports ...

/**
 * Generated by {@code StaticGsonProcessor}
 */
@Keep
public class Session_TypeAdapterFactory implements TypeAdapterFactory {
  @Override
  public <T> TypeAdapter<T> create(final Gson gson, final TypeToken<T> typeToken) {
    return (TypeAdapter<T>) new TypeAdapter<Session>() {
      @Override
      public void write(JsonWriter writer, Session value) throws IOException {
        writer.beginObject();
        writer.name("id");
        writer.value(value.id);
        writer.name("title");
        writer.value(value.title);
        writer.name("description");
        writer.value(value.description);
        writer.name("speakerId");
        writer.value(value.speakerId);
        writer.name("stime");
        writer.value(value.stime);
        writer.name("etime");
        writer.value(value.etime);
        writer.name("categoryId");
        writer.value(value.categoryId);
        writer.name("placeId");
        writer.value(value.placeId);
        writer.name("languageId");
        writer.value(value.languageId);
        writer.name("slideUrl");
        writer.value(value.slideUrl);
        writer.name("movieUrl");
        writer.value(value.movieUrl);
        writer.name("shareUrl");
        writer.value(value.shareUrl);
        writer.name("checked");
        writer.value(value.checked);
        writer.endObject();
      }

      @SuppressWarnings("unchecked")
      @Override
      public Session read(JsonReader reader) throws IOException {
        Session object = new Session();
        reader.beginObject();
        while (reader.hasNext()) {
          switch (reader.nextName()) {
            case "id":
            object.id = (int) reader.nextLong();
            break;
            case "title":
            object.title = reader.nextString();
            break;
            case "description":
            object.description = reader.nextString();
            break;
            case "speakerId":
            object.speakerId = reader.nextLong();
            break;
            case "stime":
            object.stime = reader.nextLong();
            break;
            case "etime":
            object.etime = reader.nextLong();
            break;
            case "categoryId":
            object.categoryId = reader.nextLong();
            break;
            case "placeId":
            object.placeId = reader.nextLong();
            break;
            case "languageId":
            object.languageId = reader.nextString();
            break;
            case "slideUrl":
            object.slideUrl = reader.nextString();
            break;
            case "movieUrl":
            object.movieUrl = reader.nextString();
            break;
            case "shareUrl":
            object.shareUrl = reader.nextString();
            break;
            case "checked":
            object.checked = reader.nextBoolean();
            break;
            default: break;
          }
        }
        reader.endObject();
        return (Session) object;
      }
    };
  }
}

*1:というか、互換性を失わない高速化なのでGson本体に入るのが望ましいと思います。まずGsonメンテナと交渉をしてみるつもりです。 | 2016/03/09 追記: 独立したライブラリのほうがいいだろう、ということだったのでこのまま育てることにしました。

JSON-P / JSON-BのAndroid Backportがほしい

いまはJSON処理用ライブラリが乱立していて、しかもそれぞれインターフェイスが違うので選ぶのが難しい。JSON-Object mappingはJSON-BがあるのでこれがAndroidにもbackportされればいいのにと思っている。

JSON-B (JSR367) のほうはJava8用で、参照実装もインターフェイスのデフォルト実装をバリバリつかっているのでそのままbackportするのはむずかしいのだけど、インターフェイスだけbackportすることはできるはず。そしてそのインターフェイスを実装するライブラリいくつかある、という状態が望ましいのかなと思う。

Orma v2の新機能と今後の展望

Ormaの v2.1をリリースしました。Ormaはセマンティックバージョニングを採用しているので、"v2"は単に"v1"と互換性のない変更を行った、というだけの意味です。とはいえ機能もいくつか追加しているので紹介します。

なお Orma入門はv2.1の内容にアップデートしています。

マイグレーション

v2.0, v2.1でマイグレーションの起動条件が変わりました。以前はリリースビルドとデバッグビルドでマイグレーション起動条件が違っていてデバッグが難しかったのですが、この変更によってリリースビルドとデバッグビルドで処理を同じにできたので、より安定して開発できるはずです。

これに伴い、内部もかなり変わっています。いままではSQLiteOpenHelperに依存していたのですが、v2.1ではもはや内部的にSQLiteOpenHelperは使っていません。OrmaDatabaseはOrmaConnectionにDBのopenを任せ、OrmaConnectionは単に MigrationEngine#start() を呼び出し、 MigrationEngine の実装が実際の処理を全て行います。

なお、マイグレーション起動のロジック(正確にはManualStepMigrationのロジック)の仕様自体はSQLiteOpenHelperが使う SQLiteDatabase#version (実体はSQLiteの PRAMGA user_version)に依存しているため、v1.xから移行する際、または他のSQLiteOpenHelperを使うORMから移行する際も問題にはならないはずです。

また OrmaDatabase#migrate() というメソッドでマイグレーションを明示的に起動できるようになりました。

現状の課題としては、現状はSchemaDiffMigrationでカバーできないスキーマの変更(具体的にはカラムやテーブルのリネーム)がやはりまだ難しいです。「あるカラムの名前が現状よりも古い場合に最新の名前に変える」という動的に状況を確認しつつマイグレーションを行えるようになれば改善すると思われますので、近いうちに実装するつもりです。

has-one関係の直接的な記述(direct associations)

v2.0の目玉機能です。Ormaのモデルクラスは、他のモデルクラスを直接持つことができるようになりました。

droidkaigi/konifar/Session.java がまさにこの機能を使っています。Speaker, Category, Placeが他のOrmaモデルクラスで、見ての通りJavaコード的には普通のクラスとおなじように持っているだけですね。内部的にはSessionテーブルは Speaker#id などのプライマリキーだけを持っていて、SELECT 時にまとめてとってくるようになっています。

これはまだ制限があり、v2.1.0現在は1つのモデルにつき1つだけしか持てません。2つ以上のモデルを持つためには、JOINのための複雑なテーブルエイリアスの管理が必要だからです。

モデルの関連は非常に機能が多いので、今後については不明ですが、一つ一つ実装するしかないかなーと思っています。

その他

ライブラリの新機能というわけではないですが、最近exampleアプリでOrmaが生成するコードをリポジトリにいれるようにしました。

特に *_Schema.java はモデルとSQLiteDatabaseのアダプタとなるクラスで、 Schema#bindArgs()Schema#newModelFromCursor() をみて「自分で書くのと同じだな」と思えるならOrmaを使うほうがいいということになります。

またexample/MainActivity.java にはマイグレーションのデモがあります。

RxJavaのリソース管理: イベント放出の際にisUnsubscribed()をチェックすべきだった!

f:id:gfx:20160228114108p:plain

いままでずっと勘違いしてましたが、チェックすべきなんですね。

きっかけはこれ:

これは Observable.create(OnSubscribe)onNext()onComplete() でイベントを放出する際に、Subscriber#isUnsubscribed() でsubscriberの生死をチェックすべきではないかという話です。RxJava official wikiがそうしているから、そうするべきではないかというのが議論の発端ですが、釈然としないので自分でも考えたり検証してみました。

議論の対象となるコードは Selector#executeAsObservable() です。

@NonNull
public Observable<Model> executeAsObservable() {
    return Observable.create(new Observable.OnSubscribe<Model>() {
        @Override
        public void call(final Subscriber<? super Model> subscriber) {
            forEach(new Action1<Model>() {
                @Override
                public void call(Model item) {
                    subscriber.onNext(item);
                }
            });
            subscriber.onCompleted();
        }
    });
}

これをsubscribe()してすぐunsubscribe()すると、subscribe()した側のsubscriberのonNext() などは呼ばれません。つまりこのコードは動いているように見えます。

一方で、この Observable.OnSubscribe#call() が中断されるわけではないので、イベントは送り続けます。その結果、subscribe()する側からみると、subscribe()前のmap() などの関数は依然として呼ばれます。ここが釈然としないところで、unsubscribe() によって OnSubscribe#call() が中断されるものと思っていました。

これが実際に問題になるケースは多くないと思われますが、適切にunsubscribe()をハンドルしないと無駄にCPUやメモリを使うことになるし、HTTP requestなどでは unsubscribe() の時にキャンセルしたいでしょうし、 SQLiteDatabaseも API level 16 から CancellationSignalでクエリのキャンセルを行えるので、それなりの対応は必要と思われます。

というわけで、 https://github.com/gfx/Android-Orma/pull/209 でその対応を行いました。

最初の対応は Cursor#close()Cursor#isClosed() でのハンドリングでしたが:

@NonNull
public Observable<Model> executeAsObservable() {
    return Observable.create(new Observable.OnSubscribe<Model>() {
        @Override
        public void call(final Subscriber<? super Model> subscriber) {
            final Cursor cursor = execute();
            subscriber.add(Subscriptions.create(new Action0() {
                @Override
                public void call() {
                    cursor.close();
                }
            }));
            try {
                for (int pos = 0; cursor.moveToPosition(pos); pos++) {
                    if (cursor.isClosed()) {
                        return;
                    }
                    subscriber.onNext(newModelFromCursor(cursor));
                }
            } finally {
                if (!cursor.isClosed()) {
                    cursor.close();
                }
            }
            if (cursor.isClosed()) {
                return;
            }
            subscriber.onCompleted();
        }
    });
}

複雑すぎてバグを引き起こしやすいので((実際、コードにはバグがあります。finallyのなかで Cursor#close() するので onComplete() が決して呼ばれないのです。)) isUnsubscribed() を使うコードに置き換えました。

@NonNull
public Observable<Model> executeAsObservable() {
    return Observable.create(new Observable.OnSubscribe<Model>() {
        @Override
        public void call(final Subscriber<? super Model> subscriber) {
            final Cursor cursor = execute();
            try {
                for (int pos = 0; !subscriber.isUnsubscribed() && cursor.moveToPosition(pos); pos++) {
                    subscriber.onNext(newModelFromCursor(cursor));
                }
                if (!subscriber.isUnsubscribed()) {
                    subscriber.onCompleted();
                }
            } finally {
                cursor.close();
            }
        }
    });
}

OnSubscribe#call()のなかでunsubscribe() というイベントを得る方法としては、isUnsubscribed()でイベントをpullするよりも Subscriber#add() にコールバックを設定してイベントをpushで受け取るほうが筋がいいと思いますが、今回はシンプルに書ける isUnsubscribed() を採用することにしました。

android.support.v4.util.PoolsでStringBuilderをプールしても意味なし

Pools | Android Developers

これがちょっと気になっていて、たとえば new StringBuilder() を数多く実行するケースでもしかしたら速くなるのでは?と思って試してみました。

10万回のStringBuilderを生成して文字列連結などをしています。

StringBuilderPool/MainActivity.java at master · gfx/StringBuilderPool · GitHub

しかし結果は以下のとおり、4.2.2だと多少の効果はありますが、ARTだと逆に遅くなります。今後はARTが増えていくので、StringBuilder 程度だと気にする必要はありません。そういうわけで、「もしかしたらここはPoolsで速くなるかも…」などと心配せず、安心して使うたびに new StringBuilder() したり文字列リテラルとの連結をしてください。

// MainActivity.java 内のメモ

// on Xperia A (Android 4.2.2)
// D/XXX: SimplePool: 294ms
// D/XXX: SynchronizedPool: 341ms
// D/XXX: No pool: 382ms
//
// on Xperia Z4 (Android 5.0.2)
// D/XXX: SimplePool: 152ms
// D/XXX: SynchronizedPool: 153m
// D/XXX: No pool: 116ms

Ormaのユニットテストをどう書くか、あるいはMockitoは諸刃の剣という話

短い答え

OrmaはRobolectric上でほぼ完全に動作するので、 OrmaDatabase.builder(context).name(null).build() でオンメモリDBを作ってそれを使うDAOをテストすればよい。

長い答え

Orma を使ったユニットテストは、Ormaを使うコードが正しいかどうかをテストするものであるべきです。たとえば、konifar/droidkaigi2016ではSessionDaoというクラスをつくり、Session / Speaker / Category / PlaceなどのデータをSessionというクラスを中心に操作するためのDAO: Data Access Object としています。複雑なモデルの関係があるときは、このようにDAOを作って操作するというのはいいパターンです。

このとき、DAOはロジックを含んでおり、そのロジックはテストが必要です。しかしバックエンドはOrmaなため、Ormaが動く環境を用意するか、pure JVMであればMockito*1などのモックライブラリでAndroid Frameworkをモックする必要があります。

Ormaの場合は、Robolectricの提供するSQLiteDatabaseでほぼ完全に*2動作します。実際、OrmaのすべてのユニットテストはRobolectricでもパスします。そこでDAOのロジックのテストを書くためには、単にオンメモリのOrmaDatabaseを用意してそのうえでDAOを通じた操作を行うことにすればいいでしょう。

かくして、konifar/droidkaigi2016に次のようなテストを追加しました。この場合は、アプリ内で使用している現実のデータ(session_ja.json)をロードしてなんとなくデータが構築されているかどうかをみる、というだけのテストです。開発時にこのテストがあれば、「お気に入り」まわりのロジックはTDDしたほうが開発効率はよかったと思います。

さて、このテストではMockitoを使っていません。たしかにユニットテストではMockitoを使ったほうが書きやすい事があり、とくにプラットフォーム*3が例外を投げるケースはMockitoを使わないとテストが困難なこともあります。DAOのテストも、たとえばディスクフルなどの状態をテストしたいのであればMockitoが必要でしょう。

しかし、Mockitoを使わずに書けるテストをMockitoを使って書くのは避けるべきです。Mockitoはコードの一部しか実行しないため、本当はパスしないような誤ったコードを書くことが簡単にできてしまうからです。また、Mockitoはその性質上ホワイトボックステストになり、メソッドの入出力ではなくメソッドの実装の詳細を固定してしまうことになりがちです。なので、そもそもなるべくMockitoを使わなくてすむように設計し、Mockitoを使わないようにテストを書くほうがいいと思っています。

*1:Java用のモックライブラリ

*2:ほぼというのは、一部のSQL実行時の戻り値がAndroid Frameworkのものと違うことがあるからです。しかし、普通に使う分には問題ありません。

*3:AndroidにせよJVMにせよ