Android Local TestでAndroid固有のクラスをモック化する
まえがき
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以外の部分でエラーになるようだったら、設計がよくない可能性。
皆さんもぜひ試してみてください。
参考文献
- ClassLoader (Java Platform SE 6)
- Method d in android.util.Log not mocked.に対する3つの対処法 - Qiita
- Building Local Unit Tests | Android Developers
- robolectric/robolectric: AndroidUnit Testing Framework
- jayway/powermock: PowerMock is a Java framework that allows you to unit test code normally regarded as untestable.