Android Local TestでAndroid固有のクラスをモック化する

2016.07.07

この記事は公開されてから1年以上経過しています。情報が古い可能性がありますので、ご注意ください。

まえがき

AndroidでUnit TestsでAndroid固有のクラス、メソッドを使用していると以下の様なエラーができます。

java.lang.RuntimeException: Method d in android.util.Log not mocked. See http://g.co/androidstudio/not-mocked for details.

Unit Testsで、テストするコード内にAndroidのクラスであるLog.d()を使用している時のエラーです。

いちいちLogのエラーのために、テスト時にコメントアウトするのも面倒くさいですし、テストしくにくいからLogは書かないようにするのも、デバックがしにくく開発速度に影響します。

どうにかLogクラスをテストの時に実行されない、またはエラーにならない、モック化したいと思います。

Logのモック化

先人のQiitaの記事に1つ加え(一番上の記事)、実行速度比較をしてみます。

JavaのClassLoaderの仕組みを上手くつかう

ロジック部分のテストを行うために、本来junitだけ事が足りるのに

Logのためだけに、わざわざ大きめのテストライブラリーをいれるのは大げさな気がします。

JavaのClassLoaderの仕組みをうまく使うことでLogクラス程度であれば、なんのライブラリーも入れずにMock化することできます。

同じパッケージに同じクラスがあった場合どっちがロードされるか。。。

ロードの順番で先に読み込まれたクラスがロードされれます。

つまり、ライブラリー先にロードされるので、Androidのクラスを上書きすることができます

具体的にかくと、AndroidのLogと同じパッケージでLogクラス作成します。

上書きされるので、インターフェイスは同じにしておきます。

(Log.dしか使ってない場合はdだけあれば大丈夫です)

テスト時にだけ上書きしてほしいので(project root)/app/src/test/javaに起きます。

package android.util;

public class Log {
    public static int v(String tag, String msg) {
        return 0;
    }

    public static int v(String tag, String msg, Throwable tr) {
        return 0;
    }

    public static int d(String tag, String msg) {
        return 0;
    }

    public static int d(String tag, String msg, Throwable tr) {
        return 0;
    }

    public static int i(String tag, String msg) {
        return 0;
    }

    public static int i(String tag, String msg, Throwable tr) {
        return 0;
    }

    public static int w(String tag, String msg) {
        return 0;
    }

    public static int w(String tag, String msg, Throwable tr) {
        return 0;
    }

    public static int w(String tag, Throwable tr) {
        return 0;
    }

    public static int e(String tag, String msg) {
        return 0;
    }

    public static int e(String tag, String msg, Throwable tr) {
        return 0;
    }

}

以下の3つの方法は、Qitaで解説があります。

[Method d in android.util.Log not mocked.に対する3つの対処法 - Qiita] (http://qiita.com/oxsoft/items/b12b7a6c17e92eb880cd)

  • testOptionsを設定する
  • Robolectricを使う
  • PowerMockを使う

速度比較

測定対象

1.Android Studio 2.1でEmpty Projectを作ります

apply plugin: 'com.android.application'

android {
    compileSdkVersion 24
    buildToolsVersion "24.0.0"

    defaultConfig {
        applicationId "com.classmethod.logtest"
        minSdkVersion 15
        targetSdkVersion 24
        versionCode 1
        versionName "1.0"
    }
    buildTypes {
        release {
            minifyEnabled false
            proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro'
        }
    }
}

dependencies {
    compile fileTree(dir: 'libs', include: ['*.jar'])
    testCompile 'junit:junit:4.12'
    compile 'com.android.support:appcompat-v7:24.0.0'
}

2.Logを使った適当なクラスを作ります (project root)/app/src/main/java/(app pkg)/Calclation.java

public class Calculation {
    public int sum(int x, int y) {
        Log.d("sum", x + "+" + y);
        return x + y;
    }
}

3.基本となるテストコード

(project root)/app/src/test/java/(app pkg)/CalclationTest.java

public class CalculationTest {

    @Test
    public void 加算のテストをする() {
        Calculation calculation = new Calculation();
        Assert.assertThat(calculation.sum(7, 7), Is.is(14));
    }
}

4.テスト実行 ./gradlew clean test

1回目の値はすてて、2回目移行のTotal timeの3回実行したときの平均を比較する

比較

  • Java ClassLoader式: 19.775 secs
  • testOptions式: 19.355 secs
  • Robolectric式: 25.303 secs
  • PowerMock式: 21.613 secs

まとめ

シンプルなコードためあんまり差がでませんでしたが、おすすめクラスロード式です。

理由は、以下のとおりです。

  • 一度Logクラスをつくれば、テストコードで書かなくて良い
  • ライブラリーをいれなくてよい
  • モックする時間を短縮できる
  • 早い
  • Log以外の部分でエラーになるようだったら、設計がよくない可能性。

皆さんもぜひ試してみてください。

参考文献