はじめに
事の始まりはres/raw
に置いたJsonファイルの読み込み処理が正しくできているか?というテストコードを書きたかったことです。
エミュレータを起動していちいち確認するのが嫌だったのでUnitテストを書きたかったのですが、すぐ書けるだろと慢心していたら完全に詰まったのでそこで調べたことをまとめようと思います。
初歩的で自分の無知をさらけ出す羽目になるのですが、今後同じような境遇の方の助けになればと思います。。。
対象読者
- Androidのテスト書こうとググってコピペしたけど
AndroidJUnit4.class
なんて使えない - Androidのテスト書こうとググってコピペしたけど
InstrumentationRegistry
が使えない -
AndroidTestCase
を継承するのだ!とあるけどDeprecatedですどうすれば・・ - Contextが必要なテストコードを楽にさっさと書きたい
作業環境
- Android Studio3.1
- com.android.tools.build:gradle:3.0.1
- junit:4.12
- com.android.support.test1.0.1
結論
UnitテストでContext
使う場合はRobolectric
が一番楽だと思います。
以下、サンプルです。
android {
...
// AndroidStudio3を使用している場合はこのテストオプションが必要
// 詳細は http://robolectric.org/getting-started/
testOptions {
unitTests {
includeAndroidResources = true
}
}
}
dependencies {
// 2018年1月27日時点の最新版
testImplementation "org.robolectric:robolectric:3.6.1"
}
build.gradleを設定したら対象のテストクラスで以下のようにしてContext
を取得できます。
// 1. RobolectricTestRunnerを指定する
@RunWith(RobolectricTestRunner.class)
public class HogeTest() {
@Test
public void read() {
// 2. contextを取得する
Context context = RuntimeEnvironment.application;
// 以降はcontext.getResourceやcontext.getStringなどでres情報にアクセスできる
}
}
この記事の本題
長い上にテストに精通した方なら「はあ?そんなことも知らねーのかよ」となる内容なので暖かい目で読んでください・・・
そもそもなぜ苦労したのか
最初、この作業に取りかかるとき「Android test context」といったワードでググり、src/test
配下にテストクラスを作ろうとしていました。
小難しい方法は嫌だったので、Mock作ってwhen
でgetResource
が呼ばれた時ウンヌンみたいなのは辛いから、とにかくContext
さえ取れて一番手っ取り早い方法がいい、といった具合で調べました。
そうすると以下のようなことが起こります。
-
@RunWith
でAndroidJUnit4.class
と指定しろと色々なサイトに書いてあるのに、そもそもJUnit4
しか選べない。 -
Context
はInstrumentationRegistry
で取得可能とあるけどInstrumentationRegistry
なんてクラスはないしimportできない - テストは
AndroidTestCase
を継承しgetContext
すればOK!とあるけどこのクラスはすでにDeprecatedになっていてどうすれば・・
stack overflowでAndroidJUnit4.class
は廃止されてJUnit4.class
を使うんだよとかありましたが、developer.android.com
では普通にAndroidJUnit4
使っている例があるのでそんなはずはないと思いつつも、でもbuild.gradle
で色々試しても上手くいきませんでした。
解決に辿り着いたきっかけ
しばらく考えて、ふと気になってandroidTest
のExampleInstrumentedTest.java
(最初に自動生成されるコード)を開いたらAndroidJUnit4.class
を指定しInstrumentationRegistry
を使ってContext
を取得していました。
この時点で「ああ、そうか・・なんか色々理解してなさそうだな」と私の中で色々と腑に落ちることになりました。
Local Unit TestとInstrumented Tests
再度、この2つのテストについておさらいすることにしました。
developer.android.com
のページにてLocal Unit Test
とInstrumented tests
について説明されています。
Local unit tests
はsrc/test
、Instrumented tests
はsrc/androidTest
とそれぞれディレクトリが分かれており、UnitテストはJVM上で実行され、Instrmentedテストはハードウェアデバイスやエミュレータ上でそれぞれ実行されます。そして、InstrmentedテストではContext
などUIに関するアクセスを提供するInstrumentation APIs
が使えると書かれています。
ではInstrumentation APIs
はどうやったら使えるのかというと、テスト用のAndroidManifest.xml
に指定する必要があり、それはGradle
がビルド時に自動生成してくれるようで、我々利用者側で意識する必要はありません。src/androidTest
で作成したテストクラスでのみInstrumentation APIs
を使えるのだと思われます。
また、最初に詰まったContext
を取得するために必要であろうAndroidJUnit4.class
やInstrumentationRegistry
はandroid.support.test
パッケージで定義されており、これらはUnitテストのディレクトリ(src/test
)ではimportできません。
(実際にはimportだけなら無理矢理できましたので次節で説明します。)
かなり恥ずかしい話ですが、私はandroidTest
にはUIテストを、test
にはUnitテストを書くというだいぶ大雑把な理解しかしていませんでした。
そもそもAndroidJUnit4
やInstrumentationRegistry
がtest
パッケージで使えないのは当たり前でした。
なぜLocal Unit TestではAndroidJUnit4やInstrumentationRegistryが使えない?
それぞれAndroidJUnit4
はandroid.support.test.runner
、InstrumentationRegistry
はandroid.support.test
のクラスです。
これらのクラスがsrc/test
配下に作成したUnitテストクラスでimportできない理由は、依存先が異なるためです。
build.gradle
のdependencies
では、testImplementation(またはtestCompile)
とandroidTestImplementation(またはandroidTestCompile)
の2種類が指定できます。この指定の違いは以下の通りです。
- testImplementation:
Local unit tests
に依存させる指定で、src/test
で作成したテストクラスで使用可能 - androidTestImplementation:
Instrmented Tests
に依存させる指定で、src/androidTest
で作成したテストクラスで使用可能
サンプルのbuild.gradleを例に、どの指定がどこに影響するか示します。
dependencies {
...
// 以下の指定はUnitテストに依存。src/testディレクトリで使用可能。ただ、junit4は特殊でInstrmentedテストでも利用可能
testImplementation 'junit:junit:4.12'
testImplementation "org.robolectric:robolectric:3.6.1"
// 以下の指定はInstrmentedテストに依存。src/androidTestディレクトリで使用可能
androidTestImplementation 'com.android.support.test:runner:1.0.1'
androidTestImplementation 'com.android.support.test.espresso:espresso-core:3.0.1'
}
ということは、例えばandroidTestImplementation 'com.android.support.test:runner:1.0.1'
を
testImplementation 'com.android.support.test:runner:1.0.1'
と書き換えればUnitテストでもAndroidJUnit4
やInstrumentationRegistry
がimportできる理屈になるはずです。実際、書き換えたらimportできました。
ただ、importできるだけでテスト実行時にエラーが出ました。これはInstrumentation APIs
がUnitテストでは使えないためだと思われます。
基本的にtest:runner
はInstrumentation APIs
とセットで使用する必要があるという理解であっていると思います。そもそもこんな指定ができないよう、わざわざUnitテストとInstrmentedテストを区別しているのでこの結果は当たり前でした。
公式ではありませんが、stack overflowに同じような疑問を持つ方がいて回答が参考になりました。
https://stackoverflow.com/questions/37821148/why-cannot-i-import-androidjunit4-and-activitytestrule-into-my-unit-test-class/37840321
じゃあUnitテストでContext使うには?
ここまでの話を理解すれば簡単です。私はAndroidのテスティングフレームワークが色々あって面倒臭いなと思っていました。しかし、こうした事情が理解できれば色々ある理由も分かるしどれを使えばいいかも見えてきます。今回のケースに一番当てはまるのはRobolectric
かと思います。
ここで最初に書いたまとめに辿り着きます。ほんの少しの追記でContext
をUnitテストで扱うことができるようになっていい感じです。
ただし、今回はAndroidStudio3系でかつres/rawのファイル読込テストをしたかったため、そのまま実行しようとしたら android.content.res.Resources$NotFoundException
が発生しました。
これはAndroidStudio3系ではデフォルトでAAPT2
がONになっていることが原因で、リソースファイルをテストで扱う場合は注意が必要でした。
このエラーは、まとめに書いたオプションを指定することで解消します。
なお、Droidkaigi2017のカンファレンスアプリでも同様の方法でContextを取得していたので、最初からそっちを参考にしていればもっと早く解決できた気がします。。
ただ、自分があやふやな理解であることを自覚できたため、まとまった時間をとって改めて調査できたのは良かったかなと思います。
2018年11月18日 追記
androidxに移行したらRunWith(AndroidJUnit4::class)
とInstrumentationRegistry
がdeprecatedになりました。
それぞれ定義に飛んでみるとAndroidJUnit4
はandroidx.test.ext.junit.runners.AndroidJUnit4
にしてくれとあります。
build.gradleの定義を以下の通り書き換えます。
// 修正前
androidTestImplementation 'androidx.test:runner:1.1.0'
// 修正後
androidTestImplementation 'androidx.test.ext:junit:1.0.0'
修正内容をsyncしてAndroidJUnit4をimportしなおせばOKです。
InstrumentationRegistry
はandroidx.test.platform.app.InstrumentationRegistry
にしてくれとあります。
// 修正前
val context = InstrumentationRegistry.getContext()
// 修正後
val context = InstrumentationRegistry.getInstrumentation().context
これでdeprecatedも解消されテストが通るようになりました。